Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,27 @@ I am answering this question from a framework creator perspective. I never use t

So, if you create packages for AdonisJS, I highly recommend using factory functions. Leave the `@inject` decorator for the end user.

## Conditional bindings

Use `container.bindWhen` to select a binding using values local to the resolver performing the resolution. The condition may be synchronous or asynchronous and is evaluated every time the binding is resolved.

```ts
container.bind(PaymentGateway, (resolver) => {
return resolver.make(StripePaymentGateway)
})

container.bindWhen(
PaymentGateway,
async (resolver) => {
const ctx = await resolver.make(HttpContext)
return ctx.auth.user?.featureFlags.includes('new-payment') === true
},
(resolver) => resolver.make(BetaPaymentGateway)
)
```

You may register multiple conditional bindings for the same key. Conditions are evaluated in registration order and the first match is used. When none match, the container falls back to a regular binding or its default resolution behavior.

## Binding singletons

You can bind a singleton to the container using the `container.singleton` method. It is the same as the `container.bind` method, except the factory function is called only once, and the return value is cached forever.
Expand Down Expand Up @@ -436,7 +457,7 @@ This is where the `@bind` decorator comes into the picture. To perform database

If you are using the container inside a TypeScript project, then you can define the types for all the bindings in advance at the time of creating the container instance.

Defining types will ensure the `bind`, `singleton` and `bindValue` method accepts only the known bindings and assert their types as well.
Defining types will ensure the `bind`, `bindWhen`, `singleton` and `bindValue` method accepts only the known bindings and assert their types as well.

```ts
class Route {}
Expand Down
73 changes: 72 additions & 1 deletion src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import type {
ErrorCreator,
HookCallback,
BindingValues,
BindingCondition,
BindingResolver,
ContainerOptions,
ConditionalBindings,
ContextualBindings,
} from './types.ts'

Expand Down Expand Up @@ -78,6 +80,12 @@ export class Container<KnownBindings extends Record<any, any>> {
*/
#bindings: Bindings = new Map()

/**
* Registered conditional bindings. A condition is evaluated by the resolver
* and therefore has access to values local to that resolver.
*/
#conditionalBindings: ConditionalBindings = new Map()

/**
* Registered bindings as values. The values are preferred over the bindings.
*/
Expand Down Expand Up @@ -153,6 +161,7 @@ export class Container<KnownBindings extends Record<any, any>> {
return new ContainerResolver<KnownBindings>(
{
bindings: this.#bindings,
conditionalBindings: this.#conditionalBindings,
bindingValues: this.#bindingValues,
swaps: this.#swaps,
hooks: this.#hooks,
Expand All @@ -179,7 +188,10 @@ export class Container<KnownBindings extends Record<any, any>> {
hasBinding(binding: BindingKey): boolean
hasBinding(binding: BindingKey): boolean {
return (
this.#aliases.has(binding) || this.#bindingValues.has(binding) || this.#bindings.has(binding)
this.#aliases.has(binding) ||
this.#bindingValues.has(binding) ||
this.#conditionalBindings.has(binding) ||
this.#bindings.has(binding)
)
}

Expand Down Expand Up @@ -368,6 +380,65 @@ export class Container<KnownBindings extends Record<any, any>> {
this.#bindings.set(binding, { resolver, isSingleton: false })
}

/**
* Register a binding that is used when its condition returns true. The
* condition is evaluated for every resolution and receives the resolver
* performing that resolution.
*
* Conditional bindings are evaluated in registration order. The first
* matching binding is used, otherwise resolution falls back to a regular
* binding or the default container behavior.
*
* @param binding - The binding key (string, symbol, or class constructor)
* @param condition - Predicate deciding whether the binding should be used
* @param resolver - Factory function that resolves the binding value
*
* @example
* ```ts
* container.bindWhen(
* PaymentGateway,
* async (resolver) => {
* const ctx = await resolver.make(HttpContext)
* return ctx.auth.user?.featureFlags.includes('new-payment') === true
* },
* (resolver) => resolver.make(BetaPaymentGateway)
* )
* ```
*/
bindWhen<Binding extends keyof KnownBindings>(
binding: Binding extends string | symbol ? Binding : never,
condition: BindingCondition<KnownBindings>,
resolver: BindingResolver<KnownBindings, KnownBindings[Binding]>
): void
bindWhen<Binding extends AbstractConstructor<any>>(
binding: Binding,
condition: BindingCondition<KnownBindings>,
resolver: BindingResolver<KnownBindings, InstanceType<Binding>>
): void
bindWhen<Binding>(
binding: Binding,
condition: BindingCondition<KnownBindings>,
resolver: BindingResolver<
KnownBindings,
Binding extends AbstractConstructor<infer A>
? A
: Binding extends keyof KnownBindings
? KnownBindings[Binding]
: never
>
): void {
if (typeof binding !== 'string' && typeof binding !== 'symbol' && !isClass(binding)) {
throw new InvalidArgumentsException(
'The container binding key must be of type "string", "symbol", or a "class constructor"'
)
}

debug('adding conditional binding to container "%O"', binding)
const bindings = this.#conditionalBindings.get(binding) || []
bindings.push({ condition, resolver })
this.#conditionalBindings.set(binding, bindings)
}

/**
* Register a binding as a value. Unlike bind() and singleton(), this
* method accepts the resolved value directly instead of a factory function.
Expand Down
33 changes: 33 additions & 0 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
BindingValues,
BindingResolver,
ContainerOptions,
ConditionalBindings,
ContextualBindings,
InspectableConstructor,
} from './types.ts'
Expand Down Expand Up @@ -70,6 +71,12 @@ export class ContainerResolver<KnownBindings extends Record<any, any>> {
*/
#containerBindings: Bindings

/**
* Pre-registered conditional bindings. They are shared between the container
* and resolver and evaluated using this resolver.
*/
#containerConditionalBindings: ConditionalBindings

/**
* Pre-registered bindings. They are shared between the container
* and resolver.
Expand Down Expand Up @@ -111,6 +118,7 @@ export class ContainerResolver<KnownBindings extends Record<any, any>> {
constructor(
container: {
bindings: Bindings
conditionalBindings: ConditionalBindings
bindingValues: BindingValues
swaps: Swaps
hooks: Hooks
Expand All @@ -120,6 +128,7 @@ export class ContainerResolver<KnownBindings extends Record<any, any>> {
options: ContainerOptions
) {
this.#containerBindings = container.bindings
this.#containerConditionalBindings = container.conditionalBindings
this.#containerBindingValues = container.bindingValues
this.#containerSwaps = container.swaps
this.#containerHooks = container.hooks
Expand Down Expand Up @@ -327,6 +336,29 @@ export class ContainerResolver<KnownBindings extends Record<any, any>> {
return value
}

/**
* Followed by CONDITIONAL CONTAINER bindings. Conditions are evaluated in
* registration order and the first matching resolver is used.
*/
const conditionalBindings = this.#containerConditionalBindings.get(binding)
if (conditionalBindings) {
for (const conditionalBinding of conditionalBindings) {
if (!(await conditionalBinding.condition(this, runtimeValues))) {
continue
}

const value = await conditionalBinding.resolver(this, runtimeValues)

if (debug.enabled) {
debug('resolved conditional binding %O, resolved value :%O', binding, value)
}

await this.#execHooks(binding, value)
this.#emit(binding, value)
return value
}
}

/**
* Followed by the CONTAINER bindings
*/
Expand Down Expand Up @@ -444,6 +476,7 @@ export class ContainerResolver<KnownBindings extends Record<any, any>> {
this.#containerAliases.has(binding) ||
this.#bindingValues.has(binding) ||
this.#containerBindingValues.has(binding) ||
this.#containerConditionalBindings.has(binding) ||
this.#containerBindings.has(binding)
)
}
Expand Down
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ export type BindingResolver<KnownBindings extends Record<any, any>, Value> = (
runtimeValues?: any[]
) => Value | Promise<Value>

/**
* Shape of a condition used by conditional bindings
*
* @template KnownBindings - Known bindings record type
* @param resolver - Container resolver instance
* @param runtimeValues - Optional runtime values
* @returns Whether the associated binding resolver should be used
*/
export type BindingCondition<KnownBindings extends Record<any, any>> = (
resolver: ContainerResolver<KnownBindings>,
runtimeValues?: any[]
) => boolean | Promise<boolean>

/**
* Shape of the registered bindings
*
Expand All @@ -84,6 +97,20 @@ export type Bindings = Map<
}
>

/**
* Shape of the registered conditional bindings
*
* Conditions are evaluated in registration order and the first matching
* binding is used.
*/
export type ConditionalBindings = Map<
BindingKey,
{
condition: BindingCondition<Record<any, any>>
resolver: BindingResolver<Record<any, any>, any>
}[]
>

/**
* Shape of the registered contextual bindings
*
Expand Down
Loading
Loading