module Athena::DependencyInjection

Overview

Athena's Dependency Injection (DI) component, ADI for short, adds a service container layer to your project. This useful objects, aka services, to be shared throughout the project. These objects live in a special class called the ADI::ServiceContainer (SC).

The SC is lazily initialized on fibers; this allows the SC to be accessed anywhere within the project. The Athena::DependencyInjection.container method will return the SC for the current fiber. Since the SC is defined on fibers, it allows for each fiber to have its own SC instance. This can be useful for web frameworks as each request would have its own SC scoped to that request.

NOTE It is highly recommended to use interfaces as opposed to concrete types when defining the initializers for both services and non-services. Using interfaces allows changing the functionality of a type by just changing what service gets injected into it, such as via an alias. See this blog post for an example of this.

Defined in:

Class Method Summary

Macro Summary

Class Method Detail

def self.container : ADI::ServiceContainer #

Returns the ADI::ServiceContainer for the current fiber.


Macro Detail

macro auto_configure(type, options) #

Applies the provided options to any registered service of the provided type.

A common use case of this would be to apply a specific tag to all instances of an interface; thus preventing the need to manually apply the tag for each implementation. This can be paired with Athena::DependencyInjection.bind to make working with tags easier.

It can also be used to set the public and lazy options.

Example

module ConfigInterface; end

# Automatically apply the `"config"` tag to all instances of `ConfigInterface`.
ADI.auto_configure ConfigInterface, {tags: ["config"]}

@[ADI::Register]
record ConfigOne do
  include ConfigInterface
end

@[ADI::Register]
record ConfigTwo do
  include ConfigInterface
end

# Options supplied on the annotation itself override the auto configured options.
@[ADI::Register(tags: [] of String)]
record ConfigThree do
  include ConfigInterface
end

@[ADI::Register(_configs: "!config", public: true)]
record ConfigClient, configs : Array(ConfigInterface)

ADI.container.config_client.configs # => [ConfigOne(), ConfigTwo()]

macro bind(key, value) #

Allows binding a value to a key in order to enable auto registration of that value.

Bindings allow scalar values, or those that could not otherwise be handled via service aliases, to be auto registered. This allows those arguments to be defined once and reused, as opposed to using named arguments to manually specify them for each service.

Bindings can also be declared with a type restriction to allow taking the type restriction of the argument into account. Typed bindings are always checked first as the most specific type is always preferred. If no typed bindings match the argument's type, then the last defined untyped bindings is used.

Example

module ValueInterface; end

@[ADI::Register(_value: 1, name: "value_one")]
@[ADI::Register(_value: 2, name: "value_two")]
@[ADI::Register(_value: 3, name: "value_three")]
record ValueService, value : Int32 do
  include ValueInterface
end

# Untyped bindings
ADI.bind api_key, ENV["API_KEY"]
ADI.bind config, {id: 12_i64, active: true}
ADI.bind static_value, 123
ADI.bind odd_values, ["@value_one", "@value_three"]
ADI.bind value_arr, [true, true, false]

# Typed bindings
ADI.bind value_arr : Array(Int32), [1, 2, 3]
ADI.bind value_arr : Array(Float64), [1.0, 2.0, 3.0]

@[ADI::Register(public: true)]
record BindingClient,
  api_key : String,
  config : NamedTuple(id: Int64, active: Bool),
  static_value : Int32,
  odd_values : Array(ValueInterface)

@[ADI::Register(public: true)]
record IntArr, value_arr : Array(Int32)

@[ADI::Register(public: true)]
record FloatArr, value_arr : Array(Float64)

@[ADI::Register(public: true)]
record BoolArr, value_arr : Array(Bool)

ADI.container.binding_client # =>
# BindingClient(
#  @api_key="123ABC",
#  @config={id: 12, active: true},
#  @static_value=123,
#  @odd_values=[ValueService(@value=1), ValueService(@value=3)])

ADI.container.int_arr   # => IntArr(@value_arr=[1, 2, 3])
ADI.container.float_arr # => FloatArr(@value_arr=[1.0, 2.0, 3.0])
ADI.container.bool_arr  # => BoolArr(@value_arr=[true, true, false])