Skip to content

Commit

Permalink
Allow directly injecting configuration types (#27)
Browse files Browse the repository at this point in the history
* Allow directly injecting configuration types
  • Loading branch information
Blacksmoke16 committed Mar 20, 2021
1 parent 497448a commit 55180ef
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 6 deletions.
4 changes: 2 additions & 2 deletions shard.yml
@@ -1,6 +1,6 @@
name: athena-dependency_injection

version: 0.2.6
version: 0.3.0

crystal: '>= 0.35.0'

Expand All @@ -19,7 +19,7 @@ authors:
dependencies:
athena-config:
github: athena-framework/config
version: ~> 0.2.0
version: ~> 0.3.0

development_dependencies:
ameba:
Expand Down
7 changes: 7 additions & 0 deletions spec/service_container_spec.cr
Expand Up @@ -221,5 +221,12 @@ describe Athena::DependencyInjection::ServiceContainer do
service.password.should eq "PASS"
service.credentials.should eq ["USER", "HOST"]
end

it "with configuration" do
service = ADI::ServiceContainer.new.configuration_client
service.some_config.value.should eq 123
service.nilable_config.should be_nil
service.nested_config.value.should eq 456
end
end
end
42 changes: 42 additions & 0 deletions spec/service_mocks.cr
Expand Up @@ -451,3 +451,45 @@ record ParameterClient,
username : String,
password : String,
credentials : Array(String)

#################
# CONFIGURATION #
#################
@[ACFA::Resolvable("some_config")]
record SomeConfig, value : Int32 = 123 do
def self.configure
new
end
end

@[ACFA::Resolvable("nested.cfg")]
record Nestedconfig, value : Int32 = 456

@[ACFA::Resolvable("nilable_config")]
record NilableConfig, value : Int32 = -1 do
def self.configure
nil
end
end

class Nested
getter cfg : Nestedconfig = Nestedconfig.new
end

class ACF::Base
getter some_config : SomeConfig = SomeConfig.configure
getter nilable_config : NilableConfig? = NilableConfig.configure
getter nested : Nested = Nested.new
end

@[ADI::Register(public: true)]
class ConfigurationClient
getter some_config, nilable_config, nested_config

def initialize(
@some_config : SomeConfig,
@nilable_config : NilableConfig?,
@nested_config : Nestedconfig
)
end
end
89 changes: 88 additions & 1 deletion src/athena-dependency_injection.cr
Expand Up @@ -418,7 +418,7 @@ module Athena::DependencyInjection
#
# ### Parameters
#
# The `Athena::Config` component provides a way to manage `ACF::Parameters`.
# The `Athena::Config` component provides a way to manage `ACF::Parameters` objects used to define reusable [parameters](/components/config#parameters).
# It is possible to inject these parameters directly into services in a type safe way.
#
# Parameter injection utilizes a specially formatted string, similar to tagged services.
Expand Down Expand Up @@ -451,6 +451,93 @@ module Athena::DependencyInjection
# service.db_username # => "USERNAME"
# ```
#
# ### Configuration
#
# The `Athena::Config` component provides a way to manage `ACF::Base` objects used for [configuration](/components/config#configuration).
# The `Athena::DependencyInjection` component leverages the `ACFA::Resolvable` annotation to allow injecting entire configuration objects into services
# in addition to individual [parameters][Athena::DependencyInjection::Register--parameters].
#
# The primary use case for is for types that have functionality that should be configurable by the end user.
# The configuration object could be injected as a constructor argument to set the value of instance variables, or be one itself.
#
# ```
# # Define an example configuration type for a fictional Athena component.
# # The annotation argument describes the "path" to this particular configuration
# # type from `ACF.config`. I.e. `ACF.config.some_component`.
# @[ACFA::Resolvable("some_component")]
# struct SomeComponentConfig
# # By default return a new instance with a default value.
# def self.configure : self
# new
# end
#
# getter multiplier : Int32
#
# def initialize(@multiplier : Int32 = 1); end
# end
#
# # This type would be a part of the `ACF::Base` type.
# class ACF::Base
# getter some_component : SomeComponentConfig = SomeComponentConfig.configure
# end
#
# # Define an example configurable service to use our configuration object.
# @[ADI::Register(public: true)]
# class MultiplierService
# @multiplier : Int32
#
# def initialize(config : SomeComponentConfig)
# @multiplier = config.multiplier
# end
#
# def multiply(value : Number)
# value * @multiplier
# end
# end
#
# ADI.container.multiplier_service.multiply 10 # => 10
# ```
#
# By default our `MultiplierService` will use a multiplier of `1`, the default value in the `SomeComponentConfig`.
# However, if we wanted to change that value we could do something like this, without changing any of the earlier code.
#
# ```
# # Override the configuration type's configure method
# # to supply our own custom multiplier value.
# def SomeComponentConfig.configure
# new 10
# end
#
# ADI.container.multiplier_service.multiply 10 # => 100
# ```
#
# If the configurable service is also used outside of the service container,
# the [factory][Athena::DependencyInjection::Register--factories] pattern could also be used.
#
# ```
# @[ADI::Register(public: true)]
# class MultiplierService
# # Tell the service container to use this constructor for DI.
# @[ADI::Inject]
# def self.new(config : SomeComponentConfig)
# # Using the configuration object to supply the argument to the standard initialize method.
# new config.multiplier
# end
#
# def initialize(@multiplier : Int32); end
#
# def multiply(value : Number)
# value * @multiplier
# end
# end
#
# # Multiplier from the service container.
# ADI.container.multiplier_service.multiply 10 # => 10
#
# # A directly instantiated type.
# MultiplierService.new(10).multiply 10 # => 100
# ```
#
# ### Optional Services
#
# Services defined with a nillable type restriction are considered to be optional. If no service could be resolved from the type, then `nil` is injected instead.
Expand Down
13 changes: 10 additions & 3 deletions src/service_container.cr
Expand Up @@ -241,10 +241,17 @@ class Athena::DependencyInjection::ServiceContainer
# If no services could be resolved
if resolved_services.size == 0
# Return a default value if any
if !initializer_arg.default_value.is_a? Nop

# First check to see if it's a resolvable configuration type.
if (configuration_type = initializer_arg.restriction.types.find(&.resolve.annotation ACFA::Resolvable)) && (configuration_ann = configuration_type.resolve.annotation ACFA::Resolvable)
path = configuration_ann[0] || configuration_ann["path"] || configuration_type.raise "Configuration type '#{configuration_type}' has an ACFA::Resolvable annotation but is missing the type's configuration path. It was not provided as the first positional argument nor via the 'path' field."

"ACF.config.#{path.id}".id
elsif !initializer_arg.default_value.is_a? Nop
# Otherwise fallback on a default value, if any
initializer_arg.default_value
# including `nil` if thats a possibility
elsif initializer_arg.restriction.resolve.nilable?
# including `nil` if thats a possibility
nil
else
# otherwise raise an exception
Expand All @@ -254,7 +261,7 @@ class Athena::DependencyInjection::ServiceContainer
# If only one was matched, return it,
# using an ADI::Proxy object if thats what the initializer expects.
if initializer_arg.restriction.resolve < ADI::Proxy
"ADI::Proxy.new(#{resolved_services[0]}, ->#{resolved_services[0].id.id})".id
"ADI::Proxy.new(#{resolved_services[0]}, ->#{resolved_services[0].id})".id
else
resolved_services[0].id
end
Expand Down

0 comments on commit 55180ef

Please sign in to comment.