Skip to content

Commit

Permalink
Merge pull request #2 from freshgum-bubbles/jsservice-api
Browse files Browse the repository at this point in the history
Add new JSService API
  • Loading branch information
freshgum-bubbles authored Jun 20, 2023
2 parents 2960e1f + 0ef9740 commit ee8a84e
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 65 deletions.
1 change: 1 addition & 0 deletions src/builtins.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BUILT_INS = [String, Object, Symbol, Array, Number];
162 changes: 137 additions & 25 deletions src/container-instance.class.ts

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions src/decorators/js-service.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { ServiceOptions } from "../interfaces/service-options.interface";
import { AnyConstructable } from "../types/any-constructable.type";
import { AnyInjectIdentifier } from "../types/inject-identifier.type";
import { Service } from "./service.decorator";

/**
* Marks class as a service that can be injected using Container.
* Uses the default options, wherein the class can be passed to `.get` and an instance of it will be returned.
* By default, the service shall be registered upon the `defaultContainer` container.
* @experimental
*
* @param dependencies The dependencies to provide upon initialisation of this service.
* These will be provided to the service as arguments to its constructor.
* They must be valid identifiers in the container the service shall be executed under.
*
* @param constructor The constructor of the service to inject.
*
* @returns The constructor.
*/
export function JSService<T extends AnyConstructable>(dependencies: AnyInjectIdentifier[], constructor: T): T;

/**
* Marks class as a service that can be injected using Container.
* The options allow customization of how the service is injected.
* By default, the service shall be registered upon the `defaultContainer` container.
* @experimental
*
* @param options The options to use for initialisation of the service.
* Documentation for the options can be found in ServiceOptions.
*
* @param dependencies The dependencies to provide upon initialisation of this service.
* These will be provided to the service as arguments to its constructor.
* They must be valid identifiers in the container the service shall be executed under.
*
* @param constructor The constructor of the service to inject.
*
* @returns The constructor.
*/
export function JSService<T extends AnyConstructable>(options: Omit<ServiceOptions<T>, 'dependencies'>, dependencies: AnyInjectIdentifier[], constructor: T): T;

/**
* Marks class as a service that can be injected using Container.
* The options allow customization of how the service is injected.
* By default, the service shall be registered upon the `defaultContainer` container.
*
* @param options The options to use for initialisation of the service.
* Documentation for the options can be found in ServiceOptions.
* The options must also contain the dependencies that the service requires.
*
* If found, the specified dependencies to provide upon initialisation of this service.
* These will be provided to the service as arguments to its constructor.
* They must be valid identifiers in the container the service shall be executed under.
*
* @param constructor The constructor of the service to inject.
*
* @returns The constructor.
*/
export function JSService<T extends AnyConstructable>(options: ServiceOptions<T> & { dependencies: AnyInjectIdentifier[] }, constructor: T): T;

export function JSService<T extends AnyConstructable>(
optionsOrDependencies: Omit<ServiceOptions<T>, 'dependencies'> | ServiceOptions<T> | AnyInjectIdentifier[],
dependenciesOrConstructor: AnyInjectIdentifier[] | T,
maybeConstructor?: T
): T {
let constructor!: T;

if (typeof dependenciesOrConstructor === 'function') {
constructor = dependenciesOrConstructor as T;
Service(optionsOrDependencies as ServiceOptions<T> & { dependencies: AnyInjectIdentifier[] })(constructor);
} else if (maybeConstructor) {
constructor = maybeConstructor;
Service(optionsOrDependencies, dependenciesOrConstructor)(constructor);
}

if (!constructor) {
throw new Error('The JSService overload was not used correctly.');
}

return constructor;
}

export type JSService<T> = T extends AnyConstructable<infer U> ? U : never;
10 changes: 10 additions & 0 deletions src/error/cannot-instantiate-builtin-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { CannotInstantiateValueError } from "./cannot-instantiate-value.error";

/**
* Thrown when DI encounters a service depending on a built-in type (Number, String) with no factory.
*/
export class CannotInstantiateBuiltInError extends CannotInstantiateValueError {
get message () {
return super.message + ` If your service requires built-in or unresolvable types, please use a factory.`;
}
}
3 changes: 3 additions & 0 deletions src/types/js-service.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AnyConstructable } from "./any-constructable.type";

export type JSService<T extends AnyConstructable> = T;
41 changes: 22 additions & 19 deletions test/Container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@ describe('Container', function () {
it('should be able to get a boolean', () => {
const booleanTrue = 'boolean.true';
const booleanFalse = 'boolean.false';
Container.set({ id: booleanTrue, value: true });
Container.set({ id: booleanFalse, value: false });
Container.set({ id: booleanTrue, value: true, dependencies: [] });
Container.set({ id: booleanFalse, value: false, dependencies: [] });

expect(Container.get(booleanTrue)).toBe(true);
expect(Container.get(booleanFalse)).toBe(false);
});

it('should be able to get an empty string', () => {
const emptyString = 'emptyString';
Container.set({ id: emptyString, value: '' });
Container.set({ id: emptyString, value: '', dependencies: [] });

expect(Container.get(emptyString)).toBe('');
});

it('should be able to get the 0 number', () => {
const zero = 'zero';
Container.set({ id: zero, value: 0 });
Container.set({ id: zero, value: 0, dependencies: [] });

expect(Container.get(zero)).toBe(0);
});
Expand All @@ -39,7 +39,7 @@ describe('Container', function () {
constructor(public name: string) {}
}
const testService = new TestService('this is test');
Container.set({ id: TestService, value: testService });
Container.set({ id: TestService, value: testService, dependencies: [] });
expect(Container.get(TestService)).toBe(testService);
expect(Container.get(TestService).name).toBe('this is test');
});
Expand All @@ -49,10 +49,10 @@ describe('Container', function () {
constructor(public name: string) {}
}
const firstService = new TestService('first');
Container.set({ id: 'first.service', value: firstService });
Container.set({ id: 'first.service', value: firstService, dependencies: [String] });

const secondService = new TestService('second');
Container.set({ id: 'second.service', value: secondService });
Container.set({ id: 'second.service', value: secondService, dependencies: [String] });

expect(Container.get<TestService>('first.service').name).toBe('first');
expect(Container.get<TestService>('second.service').name).toBe('second');
Expand All @@ -66,10 +66,10 @@ describe('Container', function () {
const SecondTestToken = new Token<TestService>();

const firstService = new TestService('first');
Container.set({ id: FirstTestToken, value: firstService });
Container.set({ id: FirstTestToken, value: firstService, dependencies: [] });

const secondService = new TestService('second');
Container.set({ id: SecondTestToken, value: secondService });
Container.set({ id: SecondTestToken, value: secondService, dependencies: [] });

expect(Container.get(FirstTestToken).name).toBe('first');
expect(Container.get(SecondTestToken).name).toBe('second');
Expand All @@ -82,12 +82,12 @@ describe('Container', function () {
const TestToken = new Token<TestService>();

const firstService = new TestService('first');
Container.set({ id: TestToken, value: firstService });
Container.set({ id: TestToken, value: firstService, dependencies: [String] });
expect(Container.get(TestToken)).toBe(firstService);
expect(Container.get(TestToken).name).toBe('first');

const secondService = new TestService('second');
Container.set({ id: TestToken, value: secondService });
Container.set({ id: TestToken, value: secondService, dependencies: [String] });

expect(Container.get(TestToken)).toBe(secondService);
expect(Container.get(TestToken).name).toBe('second');
Expand All @@ -108,10 +108,10 @@ describe('Container', function () {
const test1Service = new TestService();
const test2Service = new TestService();

Container.set({ id: TestService, value: testService });
Container.set({ id: 'test1-service', value: test1Service });
Container.set({ id: 'test2-service', value: test2Service });
Container.set({ id: 'test3-service', factory: [TestServiceFactory, 'create'] });
Container.set({ id: TestService, value: testService, dependencies: [] });
Container.set({ id: 'test1-service', value: test1Service, dependencies: [] });
Container.set({ id: 'test2-service', value: test2Service, dependencies: [] });
Container.set({ id: 'test3-service', factory: [TestServiceFactory, 'create'], dependencies: [] });

expect(Container.get(TestService)).toBe(testService);
expect(Container.get<TestService>('test1-service')).toBe(test1Service);
Expand All @@ -131,9 +131,9 @@ describe('Container', function () {
const test1Service = new TestService();
const test2Service = new TestService();

Container.set({ id: TestService, value: testService });
Container.set({ id: 'test1-service', value: test1Service });
Container.set({ id: 'test2-service', value: test2Service });
Container.set({ id: TestService, value: testService, dependencies: [] });
Container.set({ id: 'test1-service', value: test1Service, dependencies: [] });
Container.set({ id: 'test2-service', value: test2Service, dependencies: [] });

expect(Container.get(TestService)).toBe(testService);
expect(Container.get<TestService>('test1-service')).toBe(test1Service);
Expand All @@ -154,7 +154,7 @@ describe('Container', function () {
constructor(public name: string = 'frank') {}
}

Container.set({ id: TestService, type: TestService });
Container.set({ id: TestService, type: TestService, dependencies: [] });
const testService = Container.get(TestService);
testService.name = 'john';

Expand All @@ -179,6 +179,7 @@ describe('Container', function () {
Container.set({
id: Car,
factory: () => new Car(new Engine()),
dependencies: [Engine]
});

expect(Container.get(Car).engine.serialNumber).toBe('A-123');
Expand Down Expand Up @@ -206,6 +207,7 @@ describe('Container', function () {
Container.set({
id: Car,
factory: [CarFactory, 'createCar'],
dependencies: [Engine]
});

expect(Container.get(Car).engine.serialNumber).toBe('A-123');
Expand Down Expand Up @@ -233,6 +235,7 @@ describe('Container', function () {
Container.set({
id: VehicleService,
factory: [VehicleFactory, 'createBus'],
dependencies: []
});

expect(Container.get(VehicleService).getColor()).toBe('yellow');
Expand Down
83 changes: 83 additions & 0 deletions test/decorators/JSService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Container from '../../src/index';
import { JSService } from '../../src/decorators/js-service.decorator';
import { AnyConstructable } from '../types/any-constructable.type';

describe('JSService decorator', () => {
beforeEach(() => Container.reset({ strategy: 'resetValue' }));

type InstanceOf<T> = T extends AnyConstructable<infer U> ? U : never;

it('should take dependencies and constructor', () => {
type AnotherService = InstanceOf<typeof AnotherService>;
const AnotherService = JSService([], class AnotherService {
getMeaningOfLife () {
return 42;
}
});

type MyService = InstanceOf<typeof MyService>;
const MyService = JSService([AnotherService], class MyService {
constructor (public anotherService: AnotherService) { }
});

expect(Container.has(MyService)).toBe(true);
expect(Container.has(AnotherService)).toBe(true);
expect(Container.get(MyService).anotherService.getMeaningOfLife()).toStrictEqual(42);
});

it('should take options without dependencies, list of dependencies and constructor', () => {
type AnotherService = InstanceOf<typeof AnotherService>;
const AnotherService = JSService([], class AnotherService {
getMeaningOfLife () {
return 42;
}
});

type MyService = InstanceOf<typeof MyService>;
const MyService = JSService({ }, [AnotherService], class MyService {
public anotherService: AnotherService;

constructor (anotherService: AnotherService) {
this.anotherService = anotherService;
}
});

expect(Container.has(MyService)).toBe(true);
expect(Container.has(AnotherService)).toBe(true);
expect(Container.get(MyService).anotherService.getMeaningOfLife()).toStrictEqual(42);
});

it('should take options with dependencies and constructor', () => {
type AnotherService = InstanceOf<typeof AnotherService>;
const AnotherService = JSService([], class AnotherService {
getMeaningOfLife () {
return 42;
}
});

type MyService = InstanceOf<typeof MyService>;
const MyService = JSService({ dependencies: [AnotherService] }, class MyService {
public anotherService: AnotherService;

constructor (anotherService: AnotherService) {
this.anotherService = anotherService;
}
});

expect(Container.has(MyService)).toBe(true);
expect(Container.has(AnotherService)).toBe(true);
expect(Container.get(MyService).anotherService.getMeaningOfLife()).toStrictEqual(42);
});

it('should provide a functioning JSService type', () => {
const MyService = JSService([], class MyService {
getOne () {
return 1;
}
});

type MyService = JSService<typeof MyService>;

const myService: MyService = Container.get(MyService);
});
});
Loading

0 comments on commit ee8a84e

Please sign in to comment.