Skip to content

Commit

Permalink
docs: extend README with usage examples (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
dirkluijk authored Sep 22, 2024
1 parent 48df581 commit e310e09
Show file tree
Hide file tree
Showing 8 changed files with 573 additions and 90 deletions.
554 changes: 508 additions & 46 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"test:ci": "vitest run --silent --coverage",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"reformat": "prettier src/**/*.ts --write"
"reformat": "prettier src/**/*.ts README.md --write"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
Expand Down
6 changes: 3 additions & 3 deletions src/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ describe("Container API", () => {
});

it("inject", () => {
expect(() => inject(MyService)).toThrowError('You can only invoke inject() from the injection context');
expect(() => inject(MyService)).toThrowError("You can only invoke inject() from the injection context");

const container = new Container();
const token = new InjectionToken<MyService>("some-token");

expect(() => container.get(token)).toThrowError('No provider(s) found');
expect(() => container.get(token)).toThrowError("No provider(s) found");

container.bind({
provide: token,
Expand All @@ -34,7 +34,7 @@ describe("Container API", () => {
});

it("injectAsync", async () => {
expect(() => injectAsync(MyService)).toThrowError('You can only invoke injectAsync() from the injection context');
expect(() => injectAsync(MyService)).toThrowError("You can only invoke injectAsync() from the injection context");

const container = new Container();
const token = new InjectionToken<string>("some-token");
Expand Down
33 changes: 19 additions & 14 deletions src/decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getInjectableTargets, injectable, type InjectableClass } from './decorators.js';
import { getInjectableTargets, injectable, type InjectableClass } from "./decorators.js";

// eslint-disable-next-line @typescript-eslint/no-extraneous-class
abstract class AbstractService {}
Expand All @@ -20,23 +20,28 @@ class SpecialBazService extends BazService {}

describe("Decorators", () => {
it("should register every annotated class across its hierarchy", () => {
expect(getInjectableTargets(AbstractService as InjectableClass).map((it) => it.name)).toEqual([
"FooService",
"BarService",
"SpecialBarService",
"SpecialBazService",
]);

expect(getInjectableTargets(AbstractService as InjectableClass).map(it => it.name))
.toEqual(['FooService', 'BarService', 'SpecialBarService', 'SpecialBazService'])
expect(getInjectableTargets(FooService as InjectableClass).map((it) => it.name)).toEqual(["FooService"]);

expect(getInjectableTargets(FooService as InjectableClass).map(it => it.name))
.toEqual(['FooService'])
expect(getInjectableTargets(BarService as InjectableClass).map((it) => it.name)).toEqual([
"BarService",
"SpecialBarService",
]);

expect(getInjectableTargets(BarService as InjectableClass).map(it => it.name))
.toEqual(['BarService', 'SpecialBarService'])
expect(getInjectableTargets(SpecialBarService as InjectableClass).map((it) => it.name)).toEqual([
"SpecialBarService",
]);

expect(getInjectableTargets(SpecialBarService as InjectableClass).map(it => it.name))
.toEqual(['SpecialBarService'])
expect(getInjectableTargets(BazService as InjectableClass).map((it) => it.name)).toEqual(["SpecialBazService"]);

expect(getInjectableTargets(BazService as InjectableClass).map(it => it.name))
.toEqual(['SpecialBazService'])

expect(getInjectableTargets(SpecialBazService as InjectableClass).map(it => it.name))
.toEqual(['SpecialBazService'])
expect(getInjectableTargets(SpecialBazService as InjectableClass).map((it) => it.name)).toEqual([
"SpecialBazService",
]);
});
});
18 changes: 8 additions & 10 deletions src/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import type { Class } from "./utils.js";
import { type AbstractClass, type Class, getParentClasses } from "./utils.js";

// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
type ClassDecorator<C extends Class<unknown>> = (target: C) => C | void;

export const injectableSymbol = Symbol("injectable");

export type InjectableClass<T = unknown> = Class<T> & { [injectableSymbol]: Class<unknown>[]};
export type InjectableClass<T = unknown> = (Class<T> | AbstractClass<T>) & { [injectableSymbol]: Class<unknown>[] };

export function injectable<C extends Class<unknown>>(): ClassDecorator<C> {
return (target) => {
let superClass = Object.getPrototypeOf(target);
while (superClass.name) {
if (!Object.getOwnPropertyDescriptor(superClass, injectableSymbol)) {
Object.defineProperty(superClass, injectableSymbol, {
getParentClasses(target).forEach((parentClass) => {
if (!Object.getOwnPropertyDescriptor(parentClass, injectableSymbol)) {
Object.defineProperty(parentClass, injectableSymbol, {
value: [target],
writable: true,
});
} else {
superClass[injectableSymbol] = [...superClass[injectableSymbol], target];
parentClass[injectableSymbol] = [...parentClass[injectableSymbol], target];
}
superClass = Object.getPrototypeOf(superClass);
}
});

Object.defineProperty(target, injectableSymbol, {
value: [target],
Expand All @@ -29,7 +27,7 @@ export function injectable<C extends Class<unknown>>(): ClassDecorator<C> {
};
}

export function isInjectable<T>(target: Class<T>): target is InjectableClass<T> {
export function isInjectable<T>(target: AbstractClass<T>): target is InjectableClass<T> {
// eslint-disable-next-line no-prototype-builtins
return target.hasOwnProperty(injectableSymbol);
}
Expand Down
12 changes: 8 additions & 4 deletions src/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ describe("Container", () => {
const container = new Container();
const service = container.get(MyService);

expect(() => service.triggerInject()).toThrowError('You can only invoke inject() from the injection context');
expect(() => service.triggerInject()).toThrowError("You can only invoke inject() from the injection context");
});

it("should support all kinds of providers", async () => {
Expand Down Expand Up @@ -149,10 +149,14 @@ describe("Container", () => {

expect(factoryFn).toHaveBeenCalledTimes(1);

expect(() => container.get(tokenWithoutProvider)).toThrowError('No provider(s) found');
expect(() => container.get(tokenWithoutProvider, { optional: false })).toThrowError('No provider(s) found');
expect(() => container.get(tokenWithoutProvider)).toThrowError("No provider(s) found");
expect(() => container.get(tokenWithoutProvider, { optional: false })).toThrowError("No provider(s) found");

expect(() => container.get(tokenProvidedAsync)).toThrowError('use injectAsync() or container.getAsync() instead');
expect(() => container.get(tokenProvidedAsync)).toThrowError("use injectAsync() or container.getAsync() instead");

await container.getAsync(tokenProvidedAsync);

expect(() => container.get(tokenProvidedAsync)).not.toThrowError();

const fooAsync = await container.getAsync(tokenProvidedAsync);
expect(fooAsync).toEqual({ foo: "async" });
Expand Down
24 changes: 12 additions & 12 deletions src/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,18 +318,18 @@ describe("Providers", () => {
const MY_TOKEN = Symbol.for("my-token");

container.bindAll(
{
provide: MY_TOKEN,
useFactory: () => Promise.resolve(1),
async: true,
multi: true,
},
{
provide: MY_TOKEN,
useFactory: () => Promise.resolve(2),
async: true,
multi: true,
},
{
provide: MY_TOKEN,
useFactory: () => Promise.resolve(1),
async: true,
multi: true,
},
{
provide: MY_TOKEN,
useFactory: () => Promise.resolve(2),
async: true,
multi: true,
},
);

expect(() => container.get(MY_TOKEN)).toThrowError("use injectAsync() or container.getAsync() instead");
Expand Down
14 changes: 14 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { InjectableClass } from "./decorators.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Class<T> = new (...args: any[]) => T;

export interface AbstractClass<T> {
prototype: T;
name: string;
Expand All @@ -8,3 +11,14 @@ export interface AbstractClass<T> {
export function isClass(target: unknown): target is Class<unknown> | AbstractClass<unknown> {
return typeof target === "function";
}

export function getParentClasses(target: Class<unknown>): InjectableClass[] {
const parentClasses: InjectableClass[] = [];
let currentClass = target as InjectableClass;
while (Object.getPrototypeOf(currentClass).name) {
const parentClass: InjectableClass = Object.getPrototypeOf(currentClass);
parentClasses.push(parentClass);
currentClass = parentClass;
}
return parentClasses;
}

0 comments on commit e310e09

Please sign in to comment.