Skip to content

Commit

Permalink
Merge pull request #5 from ikenox/simplify-readme
Browse files Browse the repository at this point in the history
simplify example code
  • Loading branch information
ikenox committed Oct 15, 2023
2 parents 77bd42c + 8f586a8 commit e53a687
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 49 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: [
Expand Down
33 changes: 13 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ npm install mini-di-container
## Usage

```typescript
// =============================================
// 1. Define containers
// =============================================
import { type Infer, scope } from 'mini-di-container';

// Your types, classes and interfaces to be managed with DI container
interface Logger {
Expand All @@ -53,10 +51,10 @@ class TalkingService {
}
}

// `scope()` defines a new container scope, here example is a singleton-scoped.
// `provide(...)` defines builder methods of dependencies.
// Defines a new container scope, here example is a singleton-scoped.
const singletonScope = scope()
.provide({
// builder methods of dependencies.
logger: (): Logger => ({
log: console.log,
}),
Expand All @@ -68,36 +66,29 @@ const singletonScope = scope()
new GreetingService(config),
});
// Instanciate singleton-scoped container.
// Its contained dependencies are NOT instanciated yet, and it will be instanciated when actually used.
const singletonContainer = singletonScope.instanciate({});

// Define another-scoped container scope (as an example, here is a request-scoped).
// * It's recommended to create the scope instance as singleton regardless of its scope.
// * You can specify scope-specific external parameter (example here is `{ request: Request }`).
// * `static(...)` method allows this container to provide other-scoped container's dependency instances.
// Then the merged container keeps original scope. It means that the instances managed by the singletonContainer are still singleton.
// Define another scope. You can specify scope-specific parameter. As an example,
// here is `{ request: Request }`.
const requestScope = scope<{ request: Request }>()
.static(singletonContainer)
.provide({
// Define request-scoped dependencies.
// You can use the scope-specific external parameter to build dependencies.
talkingService: ({ greetingService }, { request }) =>
new TalkingService(
greetingService,
request.headers.get('x-greeter-name') ?? 'anonymous'
),
});

// Request-scoped container is not instanciated yet, because it is instanciated per a request.

// =============================================
// 2. Use of the defined containers
// =============================================
// The request-scoped container is not instanciated yet, since it is instanciated
// per a request.

// Your request handler as an example.
// Here assuming the request header contains `X-Greeter-Name: Alice`.
function requestHandler(request: Request) {
// Instanciate request-scoped container per request, with the scope-specific external parameter.
// Instanciate the request-scoped container per request, with the scope-specific
// external parameters.
const requestScopedContainer = requestScope.instanciate({
request,
});
Expand All @@ -108,8 +99,10 @@ function requestHandler(request: Request) {
logger.log(talkingService.talkTo('Bob')); // => 'Alice said: Hello, Bob.'

// Another usage is passing the container itself to a downstream method.
// This pattern is useful e.g. when the middreware method can't know which dependencies will be used in the downstream.
logger.log(doGreeting('Carol', requestScopedContainer)); // => 'Alice said: Hello, Carol.'
// This pattern is useful e.g. when the middreware method can't know which
// dependencies will be used in the downstream.
logger.log(doGreeting('Carol', requestScopedContainer));
// => 'Alice said: Hello, Carol.'
}

type Dependencies = Infer<typeof requestScope>;
Expand Down
33 changes: 12 additions & 21 deletions src/example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { test } from 'vitest';
import type { Infer } from './index';
import { scope } from './index';

// =============================================
// 1. Define containers
// =============================================

// Your types, classes and interfaces to be managed with DI container
interface Logger {
log(message: string): void;
Expand All @@ -29,10 +25,10 @@ class TalkingService {
}
}

// `scope()` defines a new container scope, here example is a singleton-scoped.
// `provide(...)` defines builder methods of dependencies.
// Defines a new container scope, here example is a singleton-scoped.
const singletonScope = scope()
.provide({
// builder methods of dependencies.
logger: (): Logger => ({
log: console.log,
}),
Expand All @@ -44,36 +40,29 @@ const singletonScope = scope()
new GreetingService(config),
});
// Instanciate singleton-scoped container.
// Its contained dependencies are NOT instanciated yet, and it will be instanciated when actually used.
const singletonContainer = singletonScope.instanciate({});

// Define another-scoped container scope (as an example, here is a request-scoped).
// * It's recommended to create the scope instance as singleton regardless of its scope.
// * You can specify scope-specific external parameter (example here is `{ request: Request }`).
// * `static(...)` method allows this container to provide other-scoped container's dependency instances.
// Then the merged container keeps original scope. It means that the instances managed by the singletonContainer are still singleton.
// Define another scope. You can specify scope-specific parameter. As an example,
// here is `{ request: Request }`.
const requestScope = scope<{ request: Request }>()
.static(singletonContainer)
.provide({
// Define request-scoped dependencies.
// You can use the scope-specific external parameter to build dependencies.
talkingService: ({ greetingService }, { request }) =>
new TalkingService(
greetingService,
request.headers.get('x-greeter-name') ?? 'anonymous'
),
});

// Request-scoped container is not instanciated yet, because it is instanciated per a request.

// =============================================
// 2. Use of the defined containers
// =============================================
// The request-scoped container is not instanciated yet, since it is instanciated
// per a request.

// Your request handler as an example.
// Here assuming the request header contains `X-Greeter-Name: Alice`.
function requestHandler(request: Request) {
// Instanciate request-scoped container per request, with the scope-specific external parameter.
// Instanciate the request-scoped container per request, with the scope-specific
// external parameters.
const requestScopedContainer = requestScope.instanciate({
request,
});
Expand All @@ -84,8 +73,10 @@ function requestHandler(request: Request) {
logger.log(talkingService.talkTo('Bob')); // => 'Alice said: Hello, Bob.'

// Another usage is passing the container itself to a downstream method.
// This pattern is useful e.g. when the middreware method can't know which dependencies will be used in the downstream.
logger.log(doGreeting('Carol', requestScopedContainer)); // => 'Alice said: Hello, Carol.'
// This pattern is useful e.g. when the middreware method can't know which
// dependencies will be used in the downstream.
logger.log(doGreeting('Carol', requestScopedContainer));
// => 'Alice said: Hello, Carol.'
}

type Dependencies = Infer<typeof requestScope>;
Expand Down
19 changes: 11 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export class ContainerScope<
constructor(readonly providers: Providers<Instances, Instances, ScopeArgs>) {}

/**
* Instanciates a container that provides dependency instances.
* Actually, each dependency instances are NOT instanciated yet at this point.
* It will be instanciated when actually used.
* Instanciates a container that provides dependency instances. Actually, each
* dependency instances are NOT instanciated yet at this point. It will be
* instanciated when actually used.
*/
instanciate(params: ScopeArgs): Instances {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -33,13 +33,16 @@ export class ContainerScope<
}

/**
* Merges external static instances to provide via the container.
* Merges external static objects (mainly other container instances) to provide
* via the container. It allows the container to provide other-scoped container's
* dependency instances together. The merged container keeps original scope.
*/
static<P extends Record<string, unknown> & WithoutReserved<Instances>>(
p: P
): ContainerScope<Instances & P, ScopeArgs> {
const added = Object.fromEntries(
// Don't use `Object.entries` because it causes immediate evaluation of the passed DI container's all members
// Don't use `Object.entries`
// It causes immediate evaluation of the passed DI container's all members
Object.keys(p).map((k) => [k, () => p[k]])
) as unknown as Providers<P, Instances, ScopeArgs>;
const merged = {
Expand Down Expand Up @@ -98,9 +101,9 @@ export type Infer<C extends ContainerScope<Record<never, never>, never>> =
C extends ContainerScope<infer A, never> ? A : never;

/**
* Define a new container scope.
* Container scope is like a template, or a builder of the specific container instance.
* It's preferable that each container scopes are defined at only once and reused throughout the process.
* Define a new container scope. Container scope is like a template, or a builder
* of the specific container instance. It's preferable that each container scopes
* are defined at only once and reused throughout the process.
*/
export function scope<ScopeArgs>(): ContainerScope<
Record<never, never>,
Expand Down

0 comments on commit e53a687

Please sign in to comment.