Skip to content

Commit

Permalink
feat(ioc): rewritten IoC package to use implicit deps
Browse files Browse the repository at this point in the history
  • Loading branch information
kedrzu committed Aug 16, 2023
1 parent c504c7f commit 4bfeee8
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 152 deletions.
8 changes: 4 additions & 4 deletions packages/i18n/src/Translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export interface TranslatorForModule<TKey extends string> {
}

export const Translator = defineService({
deps: {
localeProvider: LocaleProvider,
},
setup({ localeProvider }): Translator {
name: 'Translator',
setup({ inject }): Translator {
const localeProvider = inject(LocaleProvider);

const translator = (<FunctionOnly<Translator>>function (value, params) {
return translate(value, {
params,
Expand Down
56 changes: 6 additions & 50 deletions packages/ioc/src/Container.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import { Constructor, Flatten } from '@nzyme/types';

import { Executable } from './Executable.js';
import { Factory } from './Factory.js';
import { Injectable } from './Injectable.js';
import { Resolvable } from './Resolvable.js';
import { Service } from './Service.js';

export type ResolveDeps = {
[key: string]: Injectable<unknown> | Constructor<Container>;
};

export type ResolveResult<TDeps extends ResolveDeps = Record<string, Injectable>> = Flatten<{
[K in keyof TDeps]: TDeps[K] extends Injectable<infer T>
? T
: TDeps[K] extends Constructor<Container>
? Container
: never;
}>;

export class Container {
private instances = new Map<symbol, unknown>();
Expand All @@ -28,11 +12,8 @@ export class Container {
}

public set<T>(injectable: Injectable<T>, instance: T): void;
public set<T, TDeps extends ResolveDeps>(
injectable: Injectable<T>,
service: Resolvable<T, TDeps> | Factory<T, TDeps>,
): void;
public set<T>(injectable: Injectable<T>, instanceOrService: T | Service<T>): void {
public set<T>(injectable: Injectable<T>, service: Resolvable<T>): void;
public set<T>(injectable: Injectable<T>, instanceOrService: T | Resolvable<T>): void {
if (instanceOrService instanceof Resolvable) {
if (instanceOrService.for !== injectable) {
const injectableName = injectable.name ?? '';
Expand Down Expand Up @@ -68,9 +49,8 @@ export class Container {
return this.resolveInstance(injectable);
}

public execute<T, TDeps extends ResolveDeps>(executable: Executable<T, TDeps>) {
const deps = this.resolveDeps(executable.deps);
return executable.execute(deps);
public execute<T>(executable: Executable<T>) {
return this.resolve(executable)();
}

private resolveInstance(injectable: Injectable, scope?: Injectable): unknown {
Expand All @@ -89,7 +69,7 @@ export class Container {
return instance;
}

instance = this.resolveResolvable(resolver, scope);
instance = resolver.resolve(this, scope);
if (resolver.cached) {
this.instances.set(injectable.symbol, instance);
this.instances.set(resolver.symbol, instance);
Expand All @@ -99,7 +79,7 @@ export class Container {
}

if (injectable instanceof Resolvable) {
instance = this.resolveResolvable(injectable as Resolvable, scope);
instance = injectable.resolve(this, scope);

if (instance && injectable.cached) {
this.instances.set(injectable.symbol, instance);
Expand All @@ -112,28 +92,4 @@ export class Container {

return instance;
}

private resolveResolvable(resolvable: Resolvable, scope?: Injectable) {
const deps = resolvable.deps ? this.resolveDeps(resolvable.deps) : {};
return resolvable.resolve(deps, scope);
}

private resolveDeps<TDeps extends ResolveDeps>(
config: TDeps,
scope?: Injectable,
): ResolveResult<TDeps> {
const result = {} as ResolveResult<TDeps>;

for (const key in config) {
const injectable = config[key];
if (injectable === Container) {
result[key as keyof TDeps] = this as ResolveResult<TDeps>[keyof TDeps];
} else {
const value = this.resolveInstance(injectable as Injectable, scope);
result[key as keyof TDeps] = value as ResolveResult<TDeps>[keyof TDeps];
}
}

return result;
}
}
33 changes: 17 additions & 16 deletions packages/ioc/src/Executable.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { ResolveDeps, ResolveResult } from './Container.js';
import { InjectableOptions } from './Injectable.js';
import { Container } from './Container.js';
import { Injectable, InjectableOptions } from './Injectable.js';
import { Resolvable } from './Resolvable.js';

export class Executable<T, TDeps extends ResolveDeps> extends Resolvable<() => T, TDeps> {
constructor(private readonly def: ExecutableDefinition<T, TDeps>) {
export class Executable<T> extends Resolvable<() => T> {
constructor(private readonly def: ExecutableDefinition<T>) {
super(def);
}

public override get cached() {
return true;
}

override resolve(deps: ResolveResult): () => T {
return () => this.def.setup(deps as ResolveResult<TDeps>);
public override resolve(container: Container) {
return () =>
this.def.setup({
container,
inject: injectable => container.resolve(injectable),
});
}
}

public execute(deps: ResolveResult<TDeps>): void {
this.def.setup(deps);
}
export interface ExecutableContext {
readonly container: Container;
readonly inject: <T>(injectable: Injectable<T>) => T;
}

export interface ExecutableDefinition<T, TDeps extends ResolveDeps = ResolveDeps>
extends InjectableOptions {
readonly deps: TDeps;
readonly setup: (deps: ResolveResult<TDeps>) => T;
export interface ExecutableDefinition<T> extends InjectableOptions {
readonly setup: (ctx: ExecutableContext) => T;
}

export function defineExecutable<T, TDeps extends ResolveDeps>(
definition: ExecutableDefinition<T, TDeps>,
): Executable<T, TDeps> {
export function defineExecutable<T>(definition: ExecutableDefinition<T>): Executable<T> {
return new Executable(definition);
}
33 changes: 17 additions & 16 deletions packages/ioc/src/Factory.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import { EmptyObject } from '@nzyme/types';

import { ResolveDeps, ResolveResult } from './Container.js';
import { Container } from './Container.js';
import { Injectable } from './Injectable.js';
import { Resolvable, ResolvableOptions } from './Resolvable.js';

export class Factory<T, TDeps extends ResolveDeps = ResolveDeps> extends Resolvable<T, TDeps> {
constructor(private readonly def: FactoryOptions<T, TDeps>) {
export class Factory<T> extends Resolvable<T> {
constructor(private readonly def: FactoryOptions<T>) {
super(def);
}

public override get cached() {
return false;
}

override resolve(deps: ResolveResult, scope?: Injectable): T {
return this.def.setup(deps as ResolveResult<TDeps>, scope);
public override resolve(container: Container, scope?: Injectable) {
return this.def.setup({
container,
scope,
inject: injectable => container.resolve(injectable),
});
}
}

public create(deps: ResolveResult<TDeps>): T {
return this.def.setup(deps);
}
export interface FactoryContext {
readonly container: Container;
readonly scope?: Injectable;
readonly inject: <T>(injectable: Injectable<T>) => T;
}

export interface FactoryOptions<T, TDeps extends ResolveDeps = EmptyObject>
extends ResolvableOptions<T, TDeps> {
readonly setup: (deps: ResolveResult<TDeps>, scope?: Injectable) => T;
export interface FactoryOptions<T> extends ResolvableOptions<T> {
readonly setup: (ctx: FactoryContext) => T;
}

export function defineFactory<T, TDeps extends ResolveDeps = EmptyObject>(
definition: FactoryOptions<T, TDeps>,
): Factory<T, TDeps> {
export function defineFactory<T>(definition: FactoryOptions<T>): Factory<T> {
return new Factory(definition);
}
19 changes: 5 additions & 14 deletions packages/ioc/src/Resolvable.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import { EmptyObject } from '@nzyme/types';

import { ResolveDeps } from './Container.js';
import { Container } from './Container.js';
import { Injectable, InjectableOptions } from './Injectable.js';

export abstract class Resolvable<
T = unknown,
TDeps extends ResolveDeps = ResolveDeps,
> extends Injectable<T> {
export abstract class Resolvable<T = unknown> extends Injectable<T> {
public readonly for?: Injectable<T>;
public readonly deps: TDeps;

constructor(def: ResolvableOptions<T, TDeps>) {
constructor(def: ResolvableOptions<T>) {
super(def);
this.for = def.for;
this.deps = def.deps ?? ({} as TDeps);
}

public abstract get cached(): boolean;
public abstract resolve(deps: TDeps, scope?: Injectable): T | undefined;
public abstract resolve(container: Container, scope?: Injectable): T | undefined;
}

export interface ResolvableOptions<T, TDeps extends ResolveDeps = EmptyObject>
extends InjectableOptions {
readonly deps?: TDeps;
export interface ResolvableOptions<T> extends InjectableOptions {
readonly for?: Injectable<T>;
}
32 changes: 16 additions & 16 deletions packages/ioc/src/Service.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import { EmptyObject } from '@nzyme/types';

import { ResolveDeps, ResolveResult } from './Container.js';
import { Container } from './Container.js';
import { Injectable } from './Injectable.js';
import { Resolvable, ResolvableOptions } from './Resolvable.js';

export class Service<T, TDeps extends ResolveDeps = ResolveDeps> extends Resolvable<T, TDeps> {
constructor(private readonly def: ServiceOptions<T, TDeps>) {
export class Service<T> extends Resolvable<T> {
constructor(private readonly def: ServiceOptions<T>) {
super(def);
}

public override get cached() {
return true;
}

override resolve(deps: ResolveResult): T {
return this.def.setup(deps as ResolveResult<TDeps>);
public override resolve(container: Container) {
return this.def.setup({
container,
inject: injectable => container.resolve(injectable),
});
}
}

public create(deps: ResolveResult<TDeps>): T {
return this.def.setup(deps);
}
export interface ServiceContext {
readonly container: Container;
readonly inject: <T>(injectable: Injectable<T>) => T;
}

export interface ServiceOptions<T, TDeps extends ResolveDeps = EmptyObject>
extends ResolvableOptions<T, TDeps> {
readonly setup: (deps: ResolveResult<TDeps>) => T;
export interface ServiceOptions<T> extends ResolvableOptions<T> {
readonly setup: (ctx: ServiceContext) => T;
}

export function defineService<T, TDeps extends ResolveDeps = EmptyObject>(
definition: ServiceOptions<T, TDeps>,
): Service<T, TDeps> {
export function defineService<T>(definition: ServiceOptions<T>): Service<T> {
return new Service(definition);
}
15 changes: 5 additions & 10 deletions packages/ioc/tests/Factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ test('resolve factory with no deps', () => {
let count = 0;

const factory = defineFactory({
deps: {},
setup({}) {
setup() {
count++;
return 'foo';
},
Expand Down Expand Up @@ -35,7 +34,7 @@ test('resolve factory registered as injectable', () => {

const factory = defineFactory({
for: injectable,
setup({}) {
setup() {
count++;
return 'foo';
},
Expand All @@ -62,19 +61,15 @@ test('resolve service with factory dep', () => {
let count = 0;

const factory = defineFactory({
deps: {},
setup({}) {
setup() {
count++;
return 'foo';
},
});

const service = defineService({
deps: {
factory,
},
setup({ factory }) {
return factory + 'bar';
setup({ inject }) {
return inject(factory) + 'bar';
},
});

Expand Down
Loading

0 comments on commit 4bfeee8

Please sign in to comment.