From 26c0d9149098407b0bf2a723ef2e211779480350 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Mon, 24 Feb 2020 23:49:15 -0500 Subject: [PATCH 01/10] Initial prototype of DI container v3 --- src/athena-dependency_injection.cr | 44 ++++ src/service_container.cr | 309 +++++++++-------------------- 2 files changed, 137 insertions(+), 216 deletions(-) diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index ad186fe..458c217 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -23,6 +23,8 @@ alias ADI = Athena::DependencyInjection # Using interfaces allows changing the functionality of a type by just changing what service gets injected into it. # See this [blog post](https://dev.to/blacksmoke16/dependency-injection-in-crystal-2d66#plug-and-play) for an example of this. module Athena::DependencyInjection + module CompilerPass; end + # Stores metadata associated with a specific service. # # The type of the service affects how it behaves within the container. When a `struct` service is retrieved or injected into a type, it will be a copy of the one in the SC (passed by value). @@ -237,3 +239,45 @@ module Athena::DependencyInjection end end end + +@[ADI::Register("@foo")] +class Bar + include ADI::Service + + def initialize(@foo : Foo); end +end + +@[ADI::Register(1, "fred", false)] +class Foo + include ADI::Service + + def initialize(@id : Int32, @name : String, @active : Bool); end +end + +@[ADI::Register] +class Blah + include ADI::Service +end + +@[ADI::Register("@?blah")] +class Baz + include ADI::Service + + def initialize(@blah : Blah?); end +end + +@[ADI::Register(lazy: true, public: false)] +class Lazy + include ADI::Service + + def initialize + pp "new lazy" + end +end + +@[ADI::Register(public: true)] +class Public + include ADI::Service +end + +pp ADI::ServiceContainer.new diff --git a/src/service_container.cr b/src/service_container.cr index a59d7a1..4d66b40 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -3,238 +3,115 @@ # A getter is defined for each service, if it is public. # Otherwise, services are only available via constructor DI. struct Athena::DependencyInjection::ServiceContainer - # Mapping of tag name to services with that tag. - getter tags : Hash(String, Array(String)) = Hash(String, Array(String)).new - - macro finished - {% begin %} - # Define a temp hash to handle overriding services - {% services = {} of Nil => Nil %} - - {% for service in ADI::Service.includers %} - {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} - {% for ann in annotations %} - {% key = ann[:name] ? ann[:name] : service.name.split("::").last.underscore %} - {% services[key.id] = {"public" => ann[:public], "type" => service.id} %} - {% end %} - {% end %} - - # Define a `getter` in the container for each registered service. - {% for name, service in services %} - # Only make the getter public if the service is public - {% if service["public"] != true %} private {% end %}getter {{name}} : {{service["type"]}} - {% end %} - {% end %} - end - - # Initializes the container. Auto registering annotated services. - def initialize - # Work around for https://github.com/crystal-lang/crystal/issues/7975. - {{@type}} - - {% begin %} - # List of service names already registered. - {% registered_services = [] of String %} - - # Mapping of tag name to services with that tag. - {% tagged_services = {} of String => Array(String) %} - - # Generate an array of types registered with the SC, used to allow for redefining services. - {% service_types = @type.instance_vars.map(&.type.stringify) %} - - # Obtain an array of registered services, exclude any that have not already been registered. - {% services = ADI::Service.includers.select { |type| type.annotation(ADI::Register) && service_types.includes? type.stringify } %} - - # Array of services that have no dependencies. - {% no_dependency_services = services.select { |service| init = service.methods.find(&.name.==("initialize")); !init || init.args.size == 0 } %} - - # Array of services that do not have any tagged services. - # This includes any service who's arguments are strings and don't start with `!` or are not strings. - {% services_without_tagged_dependencies = services.select do |service| - initializer = service.methods.find(&.name.==("initialize")) - initializer && initializer.args.size > 0 && service.annotations(ADI::Register).all? do |service_ann| - service_ann.args.all? do |arg| - (arg.is_a?(StringLiteral) && !arg.starts_with?('!')) || !arg.is_a?(StringLiteral) - end - end - end %} - - # Array of services with tag argument. Assuming by now all other services would be registered. - # This includes any services who has at least one argument that is a string and starts with `!`. - {% services_with_tagged_dependencies = services.select do |service| - initializer = service.methods.find(&.name.==("initialize")) - initializer && service.annotations(ADI::Register).any? do |service_ann| - service_ann.args.any? do |arg| - (arg.is_a?(StringLiteral) && arg.starts_with?('!')) - end - end - end %} - - # Register the services without dependencies first - {% for service in no_dependency_services %} - {% for service_ann in service.annotations(ADI::Register) %} - {% key = service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore %} - {% tags = service_ann[:tags] ? service_ann[:tags] : [] of String %} - {% registered_services << key %} - - {% for tag in tags %} - {% tagged_services[tag] ? tagged_services[tag] << key : (tagged_services[tag] = [] of String; tagged_services[tag] << key) %} - {% end %} - - @{{key.id}} = {{service.id}}.new - {% end %} - {% end %} - - {% for service in services_without_tagged_dependencies %} - {% for service_ann in service.annotations(ADI::Register) %} - {% key = service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore %} - {% tags = service_ann[:tags] ? service_ann[:tags] : [] of String %} - {% initializer = service.methods.find(&.name.==("initialize")) %} - - {% for tag in tags %} - {% tagged_services[tag] ? tagged_services[tag] << key : (tagged_services[tag] = [] of String; tagged_services[tag] << key) %} - {% end %} - - {% if service_ann.args.all? do |arg| - if arg.is_a?(StringLiteral) && arg.starts_with?("@?") - true - elsif arg.is_a?(StringLiteral) && arg.starts_with?('@') - registered_services.includes? arg[1..-1] - else - true - end - end %} - - {% registered_services << key %} - - @{{key.id}} = {{service.id}}.new({{service_ann.args.map_with_index do |arg, idx| - if arg.is_a?(ArrayLiteral) - "#{arg.map do |array_arg| - if array_arg.is_a?(StringLiteral) && array_arg.starts_with?("@?") - # if there is no service use `nil`. - services.any? &.<=(initializer.args[idx].restriction.resolve) ? "#{array_arg[2..-1].id}".id : nil - elsif array_arg.is_a?(StringLiteral) && array_arg.starts_with?('@') - "#{array_arg[1..-1].id}".id - else - array_arg - end - end} of Union(#{initializer.args[idx].restriction.resolve.type_vars.splat})".id - elsif arg.is_a?(StringLiteral) && arg.starts_with?("@?") - # if there is no service use `nil`. - services.any? &.<=(initializer.args[idx].restriction.resolve) ? "#{arg[2..-1].id}".id : nil - elsif arg.is_a?(StringLiteral) && arg.starts_with?('@') - "#{arg[1..-1].id}".id - else - arg - end - end.splat}}) - {% else %} - {% for arg, idx in service_ann.args %} - {% if arg.is_a?(StringLiteral) && arg.starts_with?("@?") %} - # ignore - {% elsif arg.is_a?(StringLiteral) && arg.starts_with?('@') %} - {% raise "Could not resolve dependency '#{arg[1..-1].id}' for service '#{service}'. Did you forget to include `ADI::Service` or declare it optional?" unless services.any? &.<=(initializer.args[idx].restriction.resolve) %} - {% end %} - {% end %} - - {% services_without_tagged_dependencies << service %} - {% end %} - {% end %} - {% end %} - - # Lastly register services with tags, assuming their dependencies would have been resolved by now. - {% for service in services_with_tagged_dependencies %} - {% for service_ann in service.annotations(Register) %} - {% key = service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore %} - {% tags = service_ann[:tags] ? service_ann[:tags] : [] of String %} - {% initializer = service.methods.find(&.name.==("initialize")) %} - {% registered_services << key %} - - @{{key.id}} = {{service.id}}.new({{service_ann.args.map_with_index do |arg, idx| - if arg.is_a?(ArrayLiteral) - arg.map do |array_arg| - if array_arg.is_a?(StringLiteral) && array_arg.starts_with?("@?") - # if there is no service use `nil`. - services.any? &.<=(initializer.args[idx].restriction.resolve) ? "#{array_arg[2..-1].id}".id : nil - elsif array_arg.is_a?(StringLiteral) && array_arg.starts_with?('@') - "#{array_arg[1..-1].id}".id - else - array_arg - end - end - elsif arg.is_a?(StringLiteral) && arg.starts_with?("@?") - # if there is no service use `nil`. - services.any? &.<=(initializer.args[idx].restriction.resolve) ? "#{arg[2..-1].id}".id : nil - elsif arg.is_a?(StringLiteral) && arg.starts_with?('@') - "#{arg[1..-1].id}".id - elsif arg.is_a?(StringLiteral) && arg.starts_with?('!') - %(#{tagged_services[arg[1..-1]].map { |ts| "#{ts.id}".id }} of Union(#{initializer.args[idx].restriction.resolve.type_vars.splat})).id - else - arg - end - end.splat}}) - {% end %} - {% end %} - @tags = {{tagged_services}} of String => Array(String) - {% end %} + private macro stringify(string) + string.is_a?(StringLiteral) ? string : string.stringify end - # Returns an array of services of the provided *type*. - def get(type : Service.class) - get_services_by_type type + private macro is_optional_service(arg) + arg.is_a?(StringLiteral) && arg.starts_with?("@?") end - # Returns `true` if a service with the provided *name* has been registered. - def has?(name : String) : Bool - service_names.includes? name + private macro is_service(arg) + arg.is_a?(StringLiteral) && arg.starts_with?('@') end - # Returns the service of the given *type* and *name*. - # - # Raises an exception if a service could not be resolved. - def resolve(type : _, name : String) : ADI::Service - services = get_services_by_type type - - # Return the service if there is only one. - return services.first if services.size == 1 - - # # Otherwise, also use the name to resolve the service. - if (service = internal_get name) && services.includes? service - return service + private macro resolve_dependencies(service_hash, service, service_ann) + service_ann.args.map do |arg| + @type.parse_arg service_hash, service, service_ann, arg end - - # Throw an exception if it could not be resolved. - raise "Could not resolve a service with type '#{type}' and name of '#{name}'." end - # Returns services with the specified *tag*. - def tagged(tag : String) - (service_names = @tags[tag]?) ? service_names.map { |service_name| internal_get(service_name).not_nil! } : Array(ADI::Service).new + private macro parse_arg(service_hash, service, service_ann, arg) + if arg.is_a?(ArrayLiteral) + arg.map { |arr_arg| parse_arg service_hash, service, service_ann, arr_arg } + elsif @type.is_optional_service arg + key = arg[2..-1] + + if s = service_hash[key].id + "__#{key.id}".id + else + nil + end + elsif @type.is_service arg + "__#{arg[1..-1].id}".id + else + arg + end end - # Returns an `Array(ADI::Service)` for services of *type*. - private def get_services_by_type(type : T) forall T + macro finished {% begin %} - # Select the services that `T` is included in if it's a `:Module` type. - # Next, select services that are, or a child of, `T`. - {% services = @type.instance_vars.select { |iv| T.instance.resolve.includers.any?(&.<=(iv.type.resolve)) || iv.type.resolve.class <= T.resolve } %} - {% if services.empty? %}[] of ADI::Service{% else %}{{services}}{% end %} - {% end %} - end + # Define a hash to store services while the container is being built + # Key is the ID of the service and the value is another hash containing its arguments, type, etc. + {% service_hash = {} of Nil => Nil %} + + # Register each service in the hash along with some related metadata. + {% for service in ADI::Service.includers %} + {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} + {% for ann in annotations %} + {% key = ann[:name] ? ann[:name] : service.name.split("::").last.underscore %} + {% service_hash[@type.stringify(key)] = {lazy: ann[:lazy] || false, public: ann[:public] || false, type: service, service_annotation: ann} %} + {% end %} + {% end %} - # Returns a Tuple of registered service names. - private def service_names - {{{@type.instance_vars.reject(&.name.==("tags")).map(&.name.stringify).splat}}} - end + # Resolve the arguments for each service + {% for service_id, metadata in service_hash %} + {% service_hash[service_id][:arguments] = @type.resolve_dependencies service_hash, metadata[:type], metadata[:service_annotation] %} + {% end %} - # Attempts to resolve the provided *name* into a service. - private def internal_get(name : String) : ADI::Service? - {% begin %} - case name - {% for ivar in @type.instance_vars.reject(&.name.==("tags")) %} - when {{ivar.name.stringify}} then {{ivar.id}} + # Run all the compiler passes + {% for pass in ADI::CompilerPass.includers %} + {% service_hash = pass.process(service_hash) %} {% end %} + + # Define getters for the services + {% for service_id, metadata in service_hash %} + private def __{{service_id.id}} : {{metadata[:type]}} + {{metadata[:type]}}.new({% unless metadata[:arguments].empty? %}*{{metadata[:arguments]}}{% end %}) + end + + {% if metadata[:lazy] == true %} + @{{service_id.id}} : Proxy({{metadata[:type]}}) | {{metadata[:type]}} + + private def {{service_id.id}} : {{metadata[:type]}} + if (service = @{{service_id.id}}) && (service.is_a? Proxy) + @{{service_id.id}} = service.resolve + end + @{{service_id.id}}.as({{metadata[:type]}}) + end + {% else %} + @{{service_id.id}} : {{metadata[:type]}} + {% end %} + + {% if metadata[:public] == true %} + def {{service_id.id}} : {{metadata[:type]}} + previous_def + end + {% end %} + {% end %} + + # Initializes the container. Auto registering annotated services. + def initialize + # Work around for https://github.com/crystal-lang/crystal/issues/7975. + {{@type}} + + # define each service in the container + {% for service_id, metadata in service_hash %} + {% if metadata[:lazy] != true %} + @{{service_id.id}} = __{{service_id.id}} + {% else %} + @{{service_id.id}} = Proxy.new ->{ __{{service_id.id}} } + {% end %} + {% end %} end + + private record Proxy(T), obj : Proc(T) do + def resolve : T + @obj.call + end + end + + {{debug}} {% end %} end end From e6939fe5c4b9edafac535c7be7eedb1bacf224f1 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Tue, 25 Feb 2020 22:38:39 -0500 Subject: [PATCH 02/10] Support tagged services Introduce auto service resolution Introduce support for explicit arguments Refactor how services/lazy services are implemented --- spec/service_container_spec.cr | 2 +- src/athena-dependency_injection.cr | 48 ++++++++++--- src/service_container.cr | 106 ++++++++++++++++++----------- 3 files changed, 106 insertions(+), 50 deletions(-) diff --git a/spec/service_container_spec.cr b/spec/service_container_spec.cr index 4df169c..46e66e2 100644 --- a/spec/service_container_spec.cr +++ b/spec/service_container_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" -describe ADI::ServiceContainer do +pending ADI::ServiceContainer do describe "#get" do describe "by type" do it "should return an array of services with that type" do diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index 458c217..c7abdff 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -154,6 +154,7 @@ module Athena::DependencyInjection # end # ``` annotation Register; end + annotation Inject; end # Used to designate a type as a service. # @@ -240,11 +241,29 @@ module Athena::DependencyInjection end end -@[ADI::Register("@foo")] +abstract class FakeServices + include ADI::Service +end + +@[ADI::Register] +class FakeService < FakeServices + include ADI::Service + + def initialize; end +end + +@[ADI::Register(name: "custom_fake")] +class CustomFooFakeService < FakeServices + include ADI::Service + + def initialize; end +end + +@[ADI::Register(_name: "JIM")] class Bar include ADI::Service - def initialize(@foo : Foo); end + def initialize(@fake_service : FakeServices, @custom_fake : FakeServices, @name : String); end end @[ADI::Register(1, "fred", false)] @@ -266,18 +285,31 @@ class Baz def initialize(@blah : Blah?); end end -@[ADI::Register(lazy: true, public: false)] -class Lazy +@[ADI::Register(public: true)] +class Public include ADI::Service def initialize - pp "new lazy" + pp "new public" end end -@[ADI::Register(public: true)] -class Public +@[ADI::Register(lazy: true, public: true)] +class Lazy include ADI::Service + + def initialize + pp "new lazy" + end end -pp ADI::ServiceContainer.new +# cont = {"$foo": "bar", bar: 19} + +cont = ADI::ServiceContainer.new + +pp cont + +# l = cont.lazy + +# pp l +# pp l diff --git a/src/service_container.cr b/src/service_container.cr index 4d66b40..581afe8 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -15,25 +15,79 @@ struct Athena::DependencyInjection::ServiceContainer arg.is_a?(StringLiteral) && arg.starts_with?('@') end + private macro is_tagged_service(arg) + arg.is_a?(StringLiteral) && arg.starts_with?('!') + end + + private macro get_initializer_args(service) + initializer = service.methods.find(&.annotation(ADI::Inject)) || service.methods.find(&.name.==("initialize")) + if i = initializer + i.args + else + [] of Nil + end + end + private macro resolve_dependencies(service_hash, service, service_ann) - service_ann.args.map do |arg| - @type.parse_arg service_hash, service, service_ann, arg + # If positional arguments are provided, + # use them to instantiate the object + unless service_ann.args.empty? + service_ann.args.map_with_index do |arg, idx| + @type.parse_arg service_hash, service, arg, idx + end + else + # Otherwise, try and auto resolve the arguments + @type.get_initializer_args(service).map_with_index do |arg, idx| + resolved_services = [] of Nil + + service_hash.each do |service_id, metadata| + if metadata[:type] <= arg.restriction.resolve + resolved_services << service_id + end + end + + # Check if an explicit value was passed for this arg + if named_arg = service_ann.named_args["_#{arg.name}"] + @type.parse_arg service_hash, service, named_arg, idx + # If no services could be resolved + elsif resolved_services.size == 0 + # Otherwise raise an exception + arg.raise "Could not auto resolve argument #{arg}" + elsif resolved_services.size == 1 + resolved_services[0].id + else + + resolved_services.find(&.==(arg.name)).id + end + end + end + end + + private macro resolve_tags(service_hash, tag) + tagged_services = [] of Nil + service_hash.each do |service_id, metadata| + tagged_services << service_id.id if metadata[:tags].includes? tag end + tagged_services end - private macro parse_arg(service_hash, service, service_ann, arg) + private macro parse_arg(service_hash, service, arg, idx) if arg.is_a?(ArrayLiteral) - arg.map { |arr_arg| parse_arg service_hash, service, service_ann, arr_arg } + initializer = service.methods.find(&.name.==("initialize")) + + %(#{arg.map_with_index { |arr_arg, arr_idx| @type.parse_arg service_hash, service, arr_arg, arr_idx }} of Union(#{initializer.args[idx].restriction.resolve.type_vars.splat})).id elsif @type.is_optional_service arg key = arg[2..-1] - if s = service_hash[key].id - "__#{key.id}".id + if s = service_hash[key] + "#{key.id}".id else nil end elsif @type.is_service arg - "__#{arg[1..-1].id}".id + "#{arg[1..-1].id}".id + elsif @type.is_tagged_service arg + @type.resolve_tags service_hash, arg[1..-1] else arg end @@ -50,7 +104,7 @@ struct Athena::DependencyInjection::ServiceContainer {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} {% for ann in annotations %} {% key = ann[:name] ? ann[:name] : service.name.split("::").last.underscore %} - {% service_hash[@type.stringify(key)] = {lazy: ann[:lazy] || false, public: ann[:public] || false, type: service, service_annotation: ann} %} + {% service_hash[@type.stringify(key)] = {lazy: ann[:lazy] || false, public: ann[:public] || false, tags: ann[:tags] || [] of Nil, type: service, service_annotation: ann} %} {% end %} {% end %} @@ -66,28 +120,7 @@ struct Athena::DependencyInjection::ServiceContainer # Define getters for the services {% for service_id, metadata in service_hash %} - private def __{{service_id.id}} : {{metadata[:type]}} - {{metadata[:type]}}.new({% unless metadata[:arguments].empty? %}*{{metadata[:arguments]}}{% end %}) - end - - {% if metadata[:lazy] == true %} - @{{service_id.id}} : Proxy({{metadata[:type]}}) | {{metadata[:type]}} - - private def {{service_id.id}} : {{metadata[:type]}} - if (service = @{{service_id.id}}) && (service.is_a? Proxy) - @{{service_id.id}} = service.resolve - end - @{{service_id.id}}.as({{metadata[:type]}}) - end - {% else %} - @{{service_id.id}} : {{metadata[:type]}} - {% end %} - - {% if metadata[:public] == true %} - def {{service_id.id}} : {{metadata[:type]}} - previous_def - end - {% end %} + {% if metadata[:public] != true %}private{% end %} getter {{service_id.id}} : {{metadata[:type]}} { {{metadata[:type]}}.new({{metadata[:arguments].splat}}) } {% end %} # Initializes the container. Auto registering annotated services. @@ -95,22 +128,13 @@ struct Athena::DependencyInjection::ServiceContainer # Work around for https://github.com/crystal-lang/crystal/issues/7975. {{@type}} - # define each service in the container + # Initialize non lazy services {% for service_id, metadata in service_hash %} {% if metadata[:lazy] != true %} - @{{service_id.id}} = __{{service_id.id}} - {% else %} - @{{service_id.id}} = Proxy.new ->{ __{{service_id.id}} } + @{{service_id.id}} = {{service_id.id}} {% end %} {% end %} end - - private record Proxy(T), obj : Proc(T) do - def resolve : T - @obj.call - end - end - {{debug}} {% end %} end From 7d1765fae4308fded9d4a76fdcab25ddb144a6aa Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Tue, 25 Feb 2020 22:51:36 -0500 Subject: [PATCH 03/10] Support default values when auto resolving Some cleanup --- spec/compiler_spec.cr | 2 +- spec/injectable_spec.cr | 2 +- src/athena-dependency_injection.cr | 18 +++++------ src/service_container.cr | 49 ++++++++++++++++-------------- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/spec/compiler_spec.cr b/spec/compiler_spec.cr index 43a9e02..d562f36 100644 --- a/spec/compiler_spec.cr +++ b/spec/compiler_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" -describe Athena::DependencyInjection do +pending Athena::DependencyInjection do describe Athena::DependencyInjection::ServiceContainer do describe "compiler errors" do describe "when trying to access a private service directly" do diff --git a/spec/injectable_spec.cr b/spec/injectable_spec.cr index a1aa347..6f64018 100644 --- a/spec/injectable_spec.cr +++ b/spec/injectable_spec.cr @@ -1,6 +1,6 @@ require "./spec_helper" -describe ADI::Injectable do +pending ADI::Injectable do describe "with only services" do it "should inject an instance of the Store class" do klass = SomeClass.new diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index c7abdff..704bb57 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -266,6 +266,13 @@ class Bar def initialize(@fake_service : FakeServices, @custom_fake : FakeServices, @name : String); end end +@[ADI::Register] +class FooBar + include ADI::Service + + def initialize(@obj : Foo); end +end + @[ADI::Register(1, "fred", false)] class Foo include ADI::Service @@ -303,13 +310,4 @@ class Lazy end end -# cont = {"$foo": "bar", bar: 19} - -cont = ADI::ServiceContainer.new - -pp cont - -# l = cont.lazy - -# pp l -# pp l +pp ADI::ServiceContainer.new diff --git a/src/service_container.cr b/src/service_container.cr index 581afe8..badc2e8 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -21,11 +21,7 @@ struct Athena::DependencyInjection::ServiceContainer private macro get_initializer_args(service) initializer = service.methods.find(&.annotation(ADI::Inject)) || service.methods.find(&.name.==("initialize")) - if i = initializer - i.args - else - [] of Nil - end + (i = initializer) ? i.args : [] of Nil end private macro resolve_dependencies(service_hash, service, service_ann) @@ -38,26 +34,35 @@ struct Athena::DependencyInjection::ServiceContainer else # Otherwise, try and auto resolve the arguments @type.get_initializer_args(service).map_with_index do |arg, idx| - resolved_services = [] of Nil - - service_hash.each do |service_id, metadata| - if metadata[:type] <= arg.restriction.resolve - resolved_services << service_id - end - end - # Check if an explicit value was passed for this arg if named_arg = service_ann.named_args["_#{arg.name}"] @type.parse_arg service_hash, service, named_arg, idx - # If no services could be resolved - elsif resolved_services.size == 0 - # Otherwise raise an exception - arg.raise "Could not auto resolve argument #{arg}" - elsif resolved_services.size == 1 - resolved_services[0].id else + resolved_services = [] of Nil + + # Otherwise resolve possible services based on type + service_hash.each do |service_id, metadata| + if metadata[:type] <= arg.restriction.resolve + resolved_services << service_id + end + end - resolved_services.find(&.==(arg.name)).id + # If no services could be resolved + if resolved_services.size == 0 + # Return a default value if any + unless arg.default_value.is_a? Nop + arg.default_value + else + # otherwise raise an exception + arg.raise "Could not auto resolve argument #{arg}" + end + elsif resolved_services.size == 1 + # If only one was matched, return it + resolved_services[0].id + else + # Otherwise fallback on the argument's name as well + resolved_services.find(&.==(arg.name)).id + end end end end @@ -73,9 +78,7 @@ struct Athena::DependencyInjection::ServiceContainer private macro parse_arg(service_hash, service, arg, idx) if arg.is_a?(ArrayLiteral) - initializer = service.methods.find(&.name.==("initialize")) - - %(#{arg.map_with_index { |arr_arg, arr_idx| @type.parse_arg service_hash, service, arr_arg, arr_idx }} of Union(#{initializer.args[idx].restriction.resolve.type_vars.splat})).id + %(#{arg.map_with_index { |arr_arg, arr_idx| @type.parse_arg service_hash, service, arr_arg, arr_idx }} of Union(#{@type.get_initializer_args(service)[idx].restriction.resolve.type_vars.splat})).id elsif @type.is_optional_service arg key = arg[2..-1] From ebe9b132ac01975d84e7120e231832f9e91acd6f Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Thu, 27 Feb 2020 08:04:58 -0500 Subject: [PATCH 04/10] Support service aliases --- src/athena-dependency_injection.cr | 16 +++++++++++----- src/service_container.cr | 23 ++++++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index 704bb57..fec6879 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -242,7 +242,6 @@ module Athena::DependencyInjection end abstract class FakeServices - include ADI::Service end @[ADI::Register] @@ -263,7 +262,7 @@ end class Bar include ADI::Service - def initialize(@fake_service : FakeServices, @custom_fake : FakeServices, @name : String); end + def initialize(@asdf : FakeServices, @name : String); end end @[ADI::Register] @@ -285,13 +284,20 @@ class Blah include ADI::Service end -@[ADI::Register("@?blah")] -class Baz +@[ADI::Register(decorates: "blah")] +class BlahDecorator include ADI::Service - def initialize(@blah : Blah?); end + def initialize(@blah : Blah); end end +# @[ADI::Register("@?blah")] +# class Baz +# include ADI::Service + +# def initialize(@blah : Blah?); end +# end + @[ADI::Register(public: true)] class Public include ADI::Service diff --git a/src/service_container.cr b/src/service_container.cr index badc2e8..7f1cf18 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -24,7 +24,7 @@ struct Athena::DependencyInjection::ServiceContainer (i = initializer) ? i.args : [] of Nil end - private macro resolve_dependencies(service_hash, service, service_ann) + private macro resolve_dependencies(service_hash, alias_hash, service, service_ann) # If positional arguments are provided, # use them to instantiate the object unless service_ann.args.empty? @@ -61,7 +61,17 @@ struct Athena::DependencyInjection::ServiceContainer resolved_services[0].id else # Otherwise fallback on the argument's name as well - resolved_services.find(&.==(arg.name)).id + if resolved_service = resolved_services.find(&.==(arg.name)) + resolved_service.id + # If no service with that name could be resolved, + # check the alias map for the restriction + elsif aliased_service = alias_hash[arg.restriction.resolve] + # If one is found returned the aliased service + aliased_service.id + else + # Otherwise raise an exception + arg.raise "Could not auto resolve argument #{arg}" + end end end end @@ -102,18 +112,25 @@ struct Athena::DependencyInjection::ServiceContainer # Key is the ID of the service and the value is another hash containing its arguments, type, etc. {% service_hash = {} of Nil => Nil %} + # Define a hash to map alias types to a service ID. + {% alias_hash = {} of Nil => Nil %} + # Register each service in the hash along with some related metadata. {% for service in ADI::Service.includers %} {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} {% for ann in annotations %} {% key = ann[:name] ? ann[:name] : service.name.split("::").last.underscore %} {% service_hash[@type.stringify(key)] = {lazy: ann[:lazy] || false, public: ann[:public] || false, tags: ann[:tags] || [] of Nil, type: service, service_annotation: ann} %} + + {% if ann[:alias] != nil %} + {% alias_hash[ann[:alias].resolve] = key %} + {% end %} {% end %} {% end %} # Resolve the arguments for each service {% for service_id, metadata in service_hash %} - {% service_hash[service_id][:arguments] = @type.resolve_dependencies service_hash, metadata[:type], metadata[:service_annotation] %} + {% service_hash[service_id][:arguments] = @type.resolve_dependencies service_hash, alias_hash, metadata[:type], metadata[:service_annotation] %} {% end %} # Run all the compiler passes From 2883121715504828ebdbe82376238f48297a51b1 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sun, 1 Mar 2020 09:46:21 -0500 Subject: [PATCH 05/10] Specify type of tagged services array Allow getting public services by type if public Add more helper methods that could be used in compiler passes --- src/athena-dependency_injection.cr | 125 +++++++++++++++++------------ src/service_container.cr | 33 +++++--- 2 files changed, 96 insertions(+), 62 deletions(-) diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index fec6879..3ca0959 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -241,55 +241,51 @@ module Athena::DependencyInjection end end -abstract class FakeServices -end - -@[ADI::Register] -class FakeService < FakeServices - include ADI::Service - - def initialize; end -end +# abstract class FakeServices +# end -@[ADI::Register(name: "custom_fake")] -class CustomFooFakeService < FakeServices - include ADI::Service +# @[ADI::Register] +# class FakeService < FakeServices +# include ADI::Service +# end - def initialize; end -end +# @[ADI::Register(name: "custom_fake", alias: FakeServices)] +# class CustomFooFakeService < FakeServices +# include ADI::Service +# end -@[ADI::Register(_name: "JIM")] -class Bar - include ADI::Service +# @[ADI::Register(_name: "JIM")] +# class Bar +# include ADI::Service - def initialize(@asdf : FakeServices, @name : String); end -end +# def initialize(@asdf : FakeServices, @name : String); end +# end -@[ADI::Register] -class FooBar - include ADI::Service +# @[ADI::Register] +# class FooBar +# include ADI::Service - def initialize(@obj : Foo); end -end +# def initialize(@obj : Foo); end +# end -@[ADI::Register(1, "fred", false)] -class Foo - include ADI::Service +# @[ADI::Register(1, "fred", false)] +# class Foo +# include ADI::Service - def initialize(@id : Int32, @name : String, @active : Bool); end -end +# def initialize(@id : Int32, @name : String, @active : Bool); end +# end -@[ADI::Register] -class Blah - include ADI::Service -end +# @[ADI::Register] +# class Blah +# include ADI::Service +# end -@[ADI::Register(decorates: "blah")] -class BlahDecorator - include ADI::Service +# @[ADI::Register(decorates: "blah")] +# class BlahDecorator +# include ADI::Service - def initialize(@blah : Blah); end -end +# def initialize(@blah : Blah); end +# end # @[ADI::Register("@?blah")] # class Baz @@ -298,22 +294,45 @@ end # def initialize(@blah : Blah?); end # end -@[ADI::Register(public: true)] -class Public - include ADI::Service +# @[ADI::Register(public: true)] +# class Public +# include ADI::Service - def initialize - pp "new public" - end -end +# def initialize +# # pp "new public" +# end +# end -@[ADI::Register(lazy: true, public: true)] -class Lazy - include ADI::Service +# @[ADI::Register(lazy: true, public: true)] +# class Lazy +# include ADI::Service - def initialize - pp "new lazy" - end -end +# def initialize +# # pp "new lazy" +# end +# end + +# @[ADI::Register("GOOGLE", "Google", name: "google", tags: ["feed_partner", "partner"])] +# @[ADI::Register("FACEBOOK", "Facebook", name: "facebook", tags: ["partner"])] +# struct FeedPartner +# include ADI::Service + +# getter id : String +# getter name : String + +# def initialize(@id : String, @name : String); end +# end + +# @[ADI::Register("!partner")] +# class PartnerManager +# include ADI::Service + +# getter partners + +# def initialize(@partners : Array(FeedPartner)) +# end +# end + +# cont = ADI::ServiceContainer.new -pp ADI::ServiceContainer.new +# pp cont.get Public diff --git a/src/service_container.cr b/src/service_container.cr index 7f1cf18..0c5cc04 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -19,6 +19,18 @@ struct Athena::DependencyInjection::ServiceContainer arg.is_a?(StringLiteral) && arg.starts_with?('!') end + private macro get_service_key(service, service_ann) + @type.stringify(service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore) + end + + private macro get_service_hash_value(key, service, service_ann, alias_hash) + if service_ann[:alias] != nil + alias_hash[service_ann[:alias].resolve] = key + end + + {lazy: service_ann[:lazy] || false, public: service_ann[:public] || false, tags: service_ann[:tags] || [] of Nil, type: service, service_annotation: service_ann} + end + private macro get_initializer_args(service) initializer = service.methods.find(&.annotation(ADI::Inject)) || service.methods.find(&.name.==("initialize")) (i = initializer) ? i.args : [] of Nil @@ -100,7 +112,7 @@ struct Athena::DependencyInjection::ServiceContainer elsif @type.is_service arg "#{arg[1..-1].id}".id elsif @type.is_tagged_service arg - @type.resolve_tags service_hash, arg[1..-1] + %(#{@type.resolve_tags service_hash, arg[1..-1]} of Union(#{@type.get_initializer_args(service)[idx].restriction.resolve.type_vars.splat})).id else arg end @@ -119,12 +131,8 @@ struct Athena::DependencyInjection::ServiceContainer {% for service in ADI::Service.includers %} {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} {% for ann in annotations %} - {% key = ann[:name] ? ann[:name] : service.name.split("::").last.underscore %} - {% service_hash[@type.stringify(key)] = {lazy: ann[:lazy] || false, public: ann[:public] || false, tags: ann[:tags] || [] of Nil, type: service, service_annotation: ann} %} - - {% if ann[:alias] != nil %} - {% alias_hash[ann[:alias].resolve] = key %} - {% end %} + {% key = @type.get_service_key service, ann %} + {% service_hash[key] = @type.get_service_hash_value key, service, ann, alias_hash %} {% end %} {% end %} @@ -135,12 +143,19 @@ struct Athena::DependencyInjection::ServiceContainer # Run all the compiler passes {% for pass in ADI::CompilerPass.includers %} - {% service_hash = pass.process(service_hash) %} + {% service_hash = pass.process service_hash, alias_hash %} {% end %} - # Define getters for the services {% for service_id, metadata in service_hash %} + # Define a getter for the service, public if the service is public {% if metadata[:public] != true %}private{% end %} getter {{service_id.id}} : {{metadata[:type]}} { {{metadata[:type]}}.new({{metadata[:arguments].splat}}) } + + # If the service is public, also define a getter to get it via type + {% if metadata[:public] %} + def get(service : {{metadata[:type]}}.class) : {{metadata[:type]}} + {{service_id.id}} + end + {% end %} {% end %} # Initializes the container. Auto registering annotated services. From 0e1affad3bcf9b36dad4d2bbfff14aec53f1b747 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sun, 1 Mar 2020 16:06:17 -0500 Subject: [PATCH 06/10] Support nil annotation for compiler passes Have pre/post compiler passes Fix issue with explicitly providing falsely values --- src/athena-dependency_injection.cr | 10 +++++++++- src/service_container.cr | 21 +++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index 3ca0959..4e6d826 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -23,7 +23,15 @@ alias ADI = Athena::DependencyInjection # Using interfaces allows changing the functionality of a type by just changing what service gets injected into it. # See this [blog post](https://dev.to/blacksmoke16/dependency-injection-in-crystal-2d66#plug-and-play) for an example of this. module Athena::DependencyInjection - module CompilerPass; end + module CompilerPass + macro included + macro pre_process(service_hash, alias_hash) + end + + macro post_process(service_hash, alias_hash) + end + end + end # Stores metadata associated with a specific service. # diff --git a/src/service_container.cr b/src/service_container.cr index 0c5cc04..a4a2f54 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -20,15 +20,15 @@ struct Athena::DependencyInjection::ServiceContainer end private macro get_service_key(service, service_ann) - @type.stringify(service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore) + @type.stringify(service_ann && service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore) end private macro get_service_hash_value(key, service, service_ann, alias_hash) - if service_ann[:alias] != nil + if service_ann && service_ann[:alias] != nil alias_hash[service_ann[:alias].resolve] = key end - {lazy: service_ann[:lazy] || false, public: service_ann[:public] || false, tags: service_ann[:tags] || [] of Nil, type: service, service_annotation: service_ann} + {lazy: (service_ann && service_ann[:lazy]) || false, public: (service_ann && service_ann[:public]) || false, tags: (service_ann && service_ann[:tags]) || [] of Nil, type: service, service_annotation: service_ann} end private macro get_initializer_args(service) @@ -39,7 +39,7 @@ struct Athena::DependencyInjection::ServiceContainer private macro resolve_dependencies(service_hash, alias_hash, service, service_ann) # If positional arguments are provided, # use them to instantiate the object - unless service_ann.args.empty? + if service_ann && !service_ann.args.empty? service_ann.args.map_with_index do |arg, idx| @type.parse_arg service_hash, service, arg, idx end @@ -47,8 +47,8 @@ struct Athena::DependencyInjection::ServiceContainer # Otherwise, try and auto resolve the arguments @type.get_initializer_args(service).map_with_index do |arg, idx| # Check if an explicit value was passed for this arg - if named_arg = service_ann.named_args["_#{arg.name}"] - @type.parse_arg service_hash, service, named_arg, idx + if service_ann && service_ann.named_args.keys.includes? "_#{arg.name}".id + @type.parse_arg service_hash, service, service_ann.named_args["_#{arg.name}"], idx else resolved_services = [] of Nil @@ -136,14 +136,19 @@ struct Athena::DependencyInjection::ServiceContainer {% end %} {% end %} + # Run pre process compiler pass + {% for pass in ADI::CompilerPass.includers %} + {% pass.pre_process service_hash, alias_hash %} + {% end %} + # Resolve the arguments for each service {% for service_id, metadata in service_hash %} {% service_hash[service_id][:arguments] = @type.resolve_dependencies service_hash, alias_hash, metadata[:type], metadata[:service_annotation] %} {% end %} - # Run all the compiler passes + # Run post process compiler pass {% for pass in ADI::CompilerPass.includers %} - {% service_hash = pass.process service_hash, alias_hash %} + {% pass.post_process service_hash, alias_hash %} {% end %} {% for service_id, metadata in service_hash %} From 5e9944f4b1935fcc9be2b2561347675d2e9f7585 Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sun, 1 Mar 2020 18:36:15 -0500 Subject: [PATCH 07/10] Separate the publicity of a service and its alias Rename key to service_id Drop support for Injectable --- src/athena-dependency_injection.cr | 89 +++--------------------------- src/service_container.cr | 43 +++++++++++---- 2 files changed, 41 insertions(+), 91 deletions(-) diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index 4e6d826..5c7c8ed 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -173,91 +173,17 @@ module Athena::DependencyInjection def self.container : ADI::ServiceContainer Fiber.current.container end - - # Adds a new constructor that resolves the required services based on type and name. - # - # Can be included into a `class`/`struct` in order to automatically inject the required services from the container based on the type's initializer. - # - # Service lookup is based on the type restriction and name of the initializer arguments. If there is only a single service - # of the required type, then that service is used. If there are multiple services of the required type then the name of the argument's name is used. - # An exception is raised if a service was not able to be resolved. - # - # ## Examples - # - # ### Default Usage - # - # ``` - # @[ADI::Register] - # class Store - # include ADI::Service - # - # property uuid : String = "UUID" - # end - # - # class MyNonService - # include ADI::Injectable - # - # getter store : Store - # - # def initialize(@store : Store); end - # end - # - # MyNonService.new.store.uuid # => "UUID" - # ``` - # - # ### Non Service Dependencies - # - # Named arguments take precedence. This allows dependencies to be supplied explicitly without going through the resolving process; such as for testing. - # ``` - # @[ADI::Register] - # class Store - # include ADI::Service - # - # property uuid : String = "UUID" - # end - # - # class MyNonService - # include ADI::Injectable - # - # getter store : Store - # getter id : String - # - # def initialize(@store : Store, @id : String); end - # end - # - # service = MyNonService.new(id: "FOO") - # service.store.uuid # => "UUID" - # service.id # => "FOO" - # ``` - module Injectable - macro included - macro finished - {% verbatim do %} - {% if initializer = @type.methods.find &.name.stringify.==("initialize") %} - # Auto generated via `ADI::Injectable` module. - def self.new(**args) - new( - {% for arg in initializer.args %} - {{arg.name.id}}: args[{{arg.name.symbolize}}]? || ADI.container.resolve({{arg.restriction.id}}, {{arg.name.stringify}}), - {% end %} - ) - end - {% end %} - {% end %} - end - end - end end # abstract class FakeServices # end -# @[ADI::Register] +# @[ADI::Register(alias: FakeServices)] # class FakeService < FakeServices # include ADI::Service # end -# @[ADI::Register(name: "custom_fake", alias: FakeServices)] +# @[ADI::Register(name: "custom_fake")] # class CustomFooFakeService < FakeServices # include ADI::Service # end @@ -302,7 +228,7 @@ end # def initialize(@blah : Blah?); end # end -# @[ADI::Register(public: true)] +# @[ADI::Register] # class Public # include ADI::Service @@ -311,7 +237,7 @@ end # end # end -# @[ADI::Register(lazy: true, public: true)] +# @[ADI::Register(lazy: true)] # class Lazy # include ADI::Service @@ -341,6 +267,9 @@ end # end # end -# cont = ADI::ServiceContainer.new +# CONTAINER = ADI::ServiceContainer.new -# pp cont.get Public +# pp CONTAINER.get FakeServices +# pp CONTAINER.get CustomFooFakeService +# pp CONTAINER.fake_services +# pp CONTAINER.fake_service diff --git a/src/service_container.cr b/src/service_container.cr index a4a2f54..44cf7a7 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -19,16 +19,23 @@ struct Athena::DependencyInjection::ServiceContainer arg.is_a?(StringLiteral) && arg.starts_with?('!') end - private macro get_service_key(service, service_ann) + private macro get_service_id(service, service_ann) @type.stringify(service_ann && service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore) end - private macro get_service_hash_value(key, service, service_ann, alias_hash) + private macro get_service_hash_value(service_id, service, service_ann, alias_hash) if service_ann && service_ann[:alias] != nil - alias_hash[service_ann[:alias].resolve] = key + alias_hash[service_ann[:alias].resolve] = service_id end - {lazy: (service_ann && service_ann[:lazy]) || false, public: (service_ann && service_ann[:public]) || false, tags: (service_ann && service_ann[:tags]) || [] of Nil, type: service, service_annotation: service_ann} + { + lazy: (service_ann && service_ann[:lazy]) || false, + public: (service_ann && service_ann[:public]) || false, + alias_public: (service_ann && service_ann[:alias_public]) || false, + tags: (service_ann && service_ann[:tags]) || [] of Nil, + type: service.resolve, + service_annotation: service_ann + } end private macro get_initializer_args(service) @@ -102,10 +109,10 @@ struct Athena::DependencyInjection::ServiceContainer if arg.is_a?(ArrayLiteral) %(#{arg.map_with_index { |arr_arg, arr_idx| @type.parse_arg service_hash, service, arr_arg, arr_idx }} of Union(#{@type.get_initializer_args(service)[idx].restriction.resolve.type_vars.splat})).id elsif @type.is_optional_service arg - key = arg[2..-1] + service_id = arg[2..-1] - if s = service_hash[key] - "#{key.id}".id + if s = service_hash[service_id] + "#{service_id.id}".id else nil end @@ -131,8 +138,8 @@ struct Athena::DependencyInjection::ServiceContainer {% for service in ADI::Service.includers %} {% raise "#{service.name} includes `ADI::Service` but is not registered. Did you forget the annotation?" if (annotations = service.annotations(ADI::Register)) && annotations.empty? && !service.abstract? %} {% for ann in annotations %} - {% key = @type.get_service_key service, ann %} - {% service_hash[key] = @type.get_service_hash_value key, service, ann, alias_hash %} + {% service_id = @type.get_service_id service, ann %} + {% service_hash[service_id] = @type.get_service_hash_value service_id, service, ann, alias_hash %} {% end %} {% end %} @@ -151,11 +158,11 @@ struct Athena::DependencyInjection::ServiceContainer {% pass.post_process service_hash, alias_hash %} {% end %} + # Define a getter for the service, public if the service is public + # If the service is public, also define a getter to get it via type {% for service_id, metadata in service_hash %} - # Define a getter for the service, public if the service is public {% if metadata[:public] != true %}private{% end %} getter {{service_id.id}} : {{metadata[:type]}} { {{metadata[:type]}}.new({{metadata[:arguments].splat}}) } - # If the service is public, also define a getter to get it via type {% if metadata[:public] %} def get(service : {{metadata[:type]}}.class) : {{metadata[:type]}} {{service_id.id}} @@ -163,6 +170,20 @@ struct Athena::DependencyInjection::ServiceContainer {% end %} {% end %} + # Also define a getter for public aliases + {% for service_type, service_id in alias_hash %} + {% service = service_hash[service_id] %} + {% if service[:alias_public] == true %} + def {{@type.get_service_key(service_type, nil).id}} : {{service[:type]}} + {{service_id.id}} + end + + def get(service : {{service_type}}.class) : {{service[:type]}} + {{service_id.id}} + end + {% end %} + {% end %} + # Initializes the container. Auto registering annotated services. def initialize # Work around for https://github.com/crystal-lang/crystal/issues/7975. From 5232156d5d75f32aa5dd4a2f31b08af49dd8f7ed Mon Sep 17 00:00:00 2001 From: Blacksmoke16 Date: Sun, 1 Mar 2020 18:39:52 -0500 Subject: [PATCH 08/10] Use the full namespace of the type as the id by default --- src/service_container.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service_container.cr b/src/service_container.cr index 44cf7a7..5110907 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -20,7 +20,7 @@ struct Athena::DependencyInjection::ServiceContainer end private macro get_service_id(service, service_ann) - @type.stringify(service_ann && service_ann[:name] ? service_ann[:name] : service.name.split("::").last.underscore) + @type.stringify(service_ann && service_ann[:name] ? service_ann[:name] : service.name.gsub(/::/, "_").underscore) end private macro get_service_hash_value(service_id, service, service_ann, alias_hash) From f7ce1753d144ce9356838b4ed953c558c24bf726 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 8 Mar 2020 13:35:21 -0400 Subject: [PATCH 09/10] Support explicit service alias injection Some cleanup --- LICENSE | 2 +- README.md | 2 +- shard.yml | 2 +- src/athena-dependency_injection.cr | 150 +++++++++++++++-------------- 4 files changed, 80 insertions(+), 76 deletions(-) diff --git a/LICENSE b/LICENSE index 3b7826b..880c5e4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2020 Blacksmoke16 +Copyright (c) 2020 George Dietrich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 668458f..09c83ef 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ Everything is documented in the [API Docs](https://athena-framework.github.io/de ## Contributors -- [Blacksmoke16](https://github.com/blacksmoke16) - creator and maintainer +- [George Dietrich](https://github.com/blacksmoke16) - creator and maintainer diff --git a/shard.yml b/shard.yml index fff5a47..a5ef762 100644 --- a/shard.yml +++ b/shard.yml @@ -14,7 +14,7 @@ description: | Flexible instance based dependency injection service container library. authors: - - Blacksmoke16 + - George Dietrich development_dependencies: ameba: diff --git a/src/athena-dependency_injection.cr b/src/athena-dependency_injection.cr index 5c7c8ed..c4412ce 100644 --- a/src/athena-dependency_injection.cr +++ b/src/athena-dependency_injection.cr @@ -175,101 +175,105 @@ module Athena::DependencyInjection end end -# abstract class FakeServices -# end +abstract class FakeServices +end -# @[ADI::Register(alias: FakeServices)] -# class FakeService < FakeServices -# include ADI::Service -# end +@[ADI::Register] +class FakeService < FakeServices + include ADI::Service +end -# @[ADI::Register(name: "custom_fake")] -# class CustomFooFakeService < FakeServices -# include ADI::Service -# end +@[ADI::Register(name: "custom_fake", alias: FakeServices)] +class CustomFooFakeService < FakeServices + include ADI::Service +end -# @[ADI::Register(_name: "JIM")] -# class Bar -# include ADI::Service +@[ADI::Register(_name: "JIM")] +class Bar + include ADI::Service -# def initialize(@asdf : FakeServices, @name : String); end -# end + def initialize(@asdf : FakeServices, @name : String); end +end -# @[ADI::Register] -# class FooBar -# include ADI::Service +@[ADI::Register] +class FooBar + include ADI::Service -# def initialize(@obj : Foo); end -# end + def initialize(@obj : Foo); end +end -# @[ADI::Register(1, "fred", false)] -# class Foo -# include ADI::Service +@[ADI::Register(1, "fred", false)] +class Foo + include ADI::Service -# def initialize(@id : Int32, @name : String, @active : Bool); end -# end + def initialize(@id : Int32, @name : String, @active : Bool); end +end -# @[ADI::Register] -# class Blah -# include ADI::Service -# end +@[ADI::Register] +class Blah + include ADI::Service +end -# @[ADI::Register(decorates: "blah")] -# class BlahDecorator -# include ADI::Service +@[ADI::Register(decorates: "blah")] +class BlahDecorator + include ADI::Service -# def initialize(@blah : Blah); end -# end + def initialize(@blah : Blah); end +end -# @[ADI::Register("@?blah")] -# class Baz -# include ADI::Service +@[ADI::Register("@?blah")] +class Baz + include ADI::Service -# def initialize(@blah : Blah?); end -# end + def initialize(@blah : Blah?); end +end -# @[ADI::Register] -# class Public -# include ADI::Service +@[ADI::Register] +class Athena::RoutingStuff::Public + include ADI::Service -# def initialize -# # pp "new public" -# end -# end + def initialize + # pp "new public" + end +end -# @[ADI::Register(lazy: true)] -# class Lazy -# include ADI::Service +@[ADI::Register(lazy: true)] +class Lazy + include ADI::Service -# def initialize -# # pp "new lazy" -# end -# end + def initialize + # pp "new lazy" + end +end -# @[ADI::Register("GOOGLE", "Google", name: "google", tags: ["feed_partner", "partner"])] -# @[ADI::Register("FACEBOOK", "Facebook", name: "facebook", tags: ["partner"])] -# struct FeedPartner -# include ADI::Service +@[ADI::Register("GOOGLE", "Google", name: "google", tags: ["feed_partner", "partner"])] +@[ADI::Register("FACEBOOK", "Facebook", name: "facebook", tags: ["partner"])] +struct FeedPartner + include ADI::Service -# getter id : String -# getter name : String + getter id : String + getter name : String -# def initialize(@id : String, @name : String); end -# end + def initialize(@id : String, @name : String); end +end -# @[ADI::Register("!partner")] -# class PartnerManager -# include ADI::Service +@[ADI::Register("!partner")] +class PartnerManager + include ADI::Service -# getter partners + getter partners -# def initialize(@partners : Array(FeedPartner)) -# end -# end + def initialize(@partners : Array(FeedPartner)) + end +end + +@[ADI::Register("@fake_services")] +class Test + include ADI::Service + + def initialize(@t : FakeServices); end +end -# CONTAINER = ADI::ServiceContainer.new +CONTAINER = ADI::ServiceContainer.new -# pp CONTAINER.get FakeServices -# pp CONTAINER.get CustomFooFakeService -# pp CONTAINER.fake_services -# pp CONTAINER.fake_service +pp CONTAINER From 20c36e52b849864de3b7f19d81d9c356446aa97e Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sun, 8 Mar 2020 15:46:26 -0400 Subject: [PATCH 10/10] Actually push the file --- src/service_container.cr | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/service_container.cr b/src/service_container.cr index 5110907..9548e30 100644 --- a/src/service_container.cr +++ b/src/service_container.cr @@ -31,7 +31,7 @@ struct Athena::DependencyInjection::ServiceContainer { lazy: (service_ann && service_ann[:lazy]) || false, public: (service_ann && service_ann[:public]) || false, - alias_public: (service_ann && service_ann[:alias_public]) || false, + public_alias: (service_ann && service_ann[:public_alias]) || false, tags: (service_ann && service_ann[:tags]) || [] of Nil, type: service.resolve, service_annotation: service_ann @@ -86,7 +86,7 @@ struct Athena::DependencyInjection::ServiceContainer # check the alias map for the restriction elsif aliased_service = alias_hash[arg.restriction.resolve] # If one is found returned the aliased service - aliased_service.id + aliased_service.id else # Otherwise raise an exception arg.raise "Could not auto resolve argument #{arg}" @@ -111,13 +111,9 @@ struct Athena::DependencyInjection::ServiceContainer elsif @type.is_optional_service arg service_id = arg[2..-1] - if s = service_hash[service_id] - "#{service_id.id}".id - else - nil - end + (s = service_hash[service_id]) ? service_id.id : nil elsif @type.is_service arg - "#{arg[1..-1].id}".id + arg[1..-1].id elsif @type.is_tagged_service arg %(#{@type.resolve_tags service_hash, arg[1..-1]} of Union(#{@type.get_initializer_args(service)[idx].restriction.resolve.type_vars.splat})).id else @@ -158,8 +154,7 @@ struct Athena::DependencyInjection::ServiceContainer {% pass.post_process service_hash, alias_hash %} {% end %} - # Define a getter for the service, public if the service is public - # If the service is public, also define a getter to get it via type + # Define getters for each service, if the service is public, make the getter public and also define a type based getter {% for service_id, metadata in service_hash %} {% if metadata[:public] != true %}private{% end %} getter {{service_id.id}} : {{metadata[:type]}} { {{metadata[:type]}}.new({{metadata[:arguments].splat}}) } @@ -170,14 +165,13 @@ struct Athena::DependencyInjection::ServiceContainer {% end %} {% end %} - # Also define a getter for public aliases + # Define getters for aliased service, if the alias is public, make the getter public and also define a type based getter {% for service_type, service_id in alias_hash %} {% service = service_hash[service_id] %} - {% if service[:alias_public] == true %} - def {{@type.get_service_key(service_type, nil).id}} : {{service[:type]}} - {{service_id.id}} - end + {% if service[:public_alias] != true %}private{% end %} def {{@type.get_service_id(service_type, nil).id}} : {{service[:type]}}; {{service_id.id}}; end + + {% if service[:public_alias] == true %} def get(service : {{service_type}}.class) : {{service[:type]}} {{service_id.id}} end @@ -191,7 +185,7 @@ struct Athena::DependencyInjection::ServiceContainer # Initialize non lazy services {% for service_id, metadata in service_hash %} - {% if metadata[:lazy] != true %} + {% unless metadata[:lazy] == true %} @{{service_id.id}} = {{service_id.id}} {% end %} {% end %}