From ce0ea753d253aadc8bf76679cc72e3780aa34cb7 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Mon, 16 Jan 2023 15:21:56 -0500 Subject: [PATCH 1/4] add event dispatcher ext to DI component --- .../src/athena-dependency_injection.cr | 3 + .../src/ext/event_dispatcher.cr | 101 ++++++++++++++++++ src/components/framework/src/athena.cr | 10 +- .../framework/src/ext/event_dispatcher.cr | 7 -- .../register_event_listeners.cr | 85 --------------- 5 files changed, 108 insertions(+), 98 deletions(-) create mode 100644 src/components/dependency_injection/src/ext/event_dispatcher.cr delete mode 100644 src/components/framework/src/ext/event_dispatcher.cr delete mode 100644 src/components/framework/src/ext/event_dispatcher/register_event_listeners.cr diff --git a/src/components/dependency_injection/src/athena-dependency_injection.cr b/src/components/dependency_injection/src/athena-dependency_injection.cr index c6bfdc73e..84392eaa1 100644 --- a/src/components/dependency_injection/src/athena-dependency_injection.cr +++ b/src/components/dependency_injection/src/athena-dependency_injection.cr @@ -184,3 +184,6 @@ module Athena::DependencyInjection Fiber.current.container end end + +# Require extension code last so all built-in DI types are available +require "./ext/*" diff --git a/src/components/dependency_injection/src/ext/event_dispatcher.cr b/src/components/dependency_injection/src/ext/event_dispatcher.cr new file mode 100644 index 000000000..4ea134f40 --- /dev/null +++ b/src/components/dependency_injection/src/ext/event_dispatcher.cr @@ -0,0 +1,101 @@ +# TODO: Clean this up once https://github.com/crystal-lang/crystal/issues/12965 is resolved +{% skip_file unless @top_level.has_constant?("Athena") && Athena.has_constant?("EventDispatcher") && Athena::EventDispatcher.has_constant?("EventDispatcher") %} + +@[ADI::Register(name: "event_dispatcher", alias: AED::EventDispatcherInterface)] +class AED::EventDispatcher +end + +ADI.auto_configure AED::EventListenerInterface, {tags: [ADI::EventDispatcher::Listeners::TAG]} + +# :nodoc: +module Athena::DependencyInjection::EventDispatcher + module Listeners + TAG = "athena.event_dispatcher.listener" + end + + module CompilerPasses::RegisterEventListenersPass + include Athena::DependencyInjection::PreArgumentsCompilerPass + + macro included + macro finished + {% verbatim do %} + {% + SERVICE_HASH["event_dispatcher"][:factory] = {"self".id, "get_event_dispatcher"} + + TAG_HASH[ADI::EventDispatcher::Listeners::TAG].each do |service_id| + SERVICE_HASH[service_id][:visibility] = Visibility::INTERNAL + end + %} + + private def get_event_dispatcher + dispatcher = AED::EventDispatcher.new + + {% for service_id in TAG_HASH[ADI::EventDispatcher::Listeners::TAG] %} + {% metadata = SERVICE_HASH[service_id] %} + + {% listeners = [] of Nil %} + + # Changes made here should also be reflected within `AED::EventListenerInterface` overload within `AED::EventDispatcher`. + {% + class_listeners = metadata[:service].class.methods.select &.annotation(AEDA::AsEventListener) + + # Raise compile time error if a listener is defined as a class method. + unless class_listeners.empty? + class_listeners.first.raise "Event listener methods can only be defined as instance methods. Did you mean '#{metadata[:service].name}##{class_listeners.first.name}'?" + end + + metadata[:service].methods.select(&.annotation(AEDA::AsEventListener)).each do |m| + # Validate the parameters of each method. + if (m.args.size < 1) || (m.args.size > 2) + m.raise "Expected '#{metadata[:service].name}##{m.name}' to have 1..2 parameters, got '#{m.args.size}'." + end + + event_arg = m.args[0] + + # Validate the type restriction of the first parameter, if present + event_arg.raise "Expected parameter #1 of '#{metadata[:service].name}##{m.name}' to have a type restriction of an 'AED::Event' instance, but it is not restricted." if event_arg.restriction.is_a?(Nop) + event_arg.raise "Expected parameter #1 of '#{metadata[:service].name}##{m.name}' to have a type restriction of an 'AED::Event' instance, not '#{event_arg.restriction}'." if !(event_arg.restriction.resolve <= AED::Event) + + if dispatcher_arg = m.args[1] + event_arg.raise "Expected parameter #2 of '#{metadata[:service].name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted." if dispatcher_arg.restriction.is_a?(Nop) + event_arg.raise "Expected parameter #2 of '#{metadata[:service].name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', not '#{dispatcher_arg.restriction}'." if !(dispatcher_arg.restriction.resolve <= AED::EventDispatcherInterface) + end + + priority = m.annotation(AEDA::AsEventListener)[:priority] || 0 + + unless priority.is_a? NumberLiteral + m.raise "Event listener method '#{metadata[:service].name}##{m.name}' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a '#{priority.class_name.id}'." + end + + listeners << { + event_arg.restriction.resolve.id, + m.args.size, + m.name.id, + "#{metadata[:service]}##{m.name.id}", + priority, + } + end + %} + + {% for info in listeners %} + {% event, count, method, name, priority = info %} + + {% if 1 == count %} + dispatcher.add_callable( + {{event}}.callable(priority: {{priority}}, name: {{name}}) { |event| self.{{service_id.id}}.{{method}} event.as({{event}}) }, + ) + {% else %} + dispatcher.add_callable( + {{event}}.callable(priority: {{priority}}, name: {{name}}) { |event, dispatcher| self.{{service_id.id}}.{{method}} event.as({{event}}), dispatcher }, + ) + {% end %} + {% end %} + {% end %} + + dispatcher + end + {% end %} + end + end + end +end diff --git a/src/components/framework/src/athena.cr b/src/components/framework/src/athena.cr index 1ec25834a..6a75ac05c 100755 --- a/src/components/framework/src/athena.cr +++ b/src/components/framework/src/athena.cr @@ -4,10 +4,12 @@ require "json" require "athena-config" require "athena-console" -require "athena-dependency_injection" require "athena-event_dispatcher" require "athena-negotiation" +# Require DI component last so it knows what extensions it should load +require "athena-dependency_injection" + require "./action" require "./annotations" require "./binary_file_response" @@ -41,7 +43,6 @@ require "./view/*" require "./ext/console" require "./ext/conversion_types" -require "./ext/event_dispatcher" require "./ext/negotiation" require "./ext/routing" require "./ext/validator" @@ -106,10 +107,7 @@ module Athena::Framework # The `AED::EventListenerInterface` that act upon `ATH::Events` to handle a request. Custom listeners can also be defined, see `AED::EventListenerInterface`. # # See each listener and the [external documentation](/components/event_dispatcher/) for more information. - module Listeners - # The tag name for Athena event listeners. - TAG = "athena.event_dispatcher.listener" - end + module Listeners; end # Namespace for types related to request parameter processing. # diff --git a/src/components/framework/src/ext/event_dispatcher.cr b/src/components/framework/src/ext/event_dispatcher.cr deleted file mode 100644 index 4db1b7969..000000000 --- a/src/components/framework/src/ext/event_dispatcher.cr +++ /dev/null @@ -1,7 +0,0 @@ -require "./event_dispatcher/*" - -@[ADI::Register(name: "event_dispatcher", alias: AED::EventDispatcherInterface)] -class AED::EventDispatcher -end - -ADI.auto_configure AED::EventListenerInterface, {tags: [ATH::Listeners::TAG]} diff --git a/src/components/framework/src/ext/event_dispatcher/register_event_listeners.cr b/src/components/framework/src/ext/event_dispatcher/register_event_listeners.cr deleted file mode 100644 index 64a62f7d3..000000000 --- a/src/components/framework/src/ext/event_dispatcher/register_event_listeners.cr +++ /dev/null @@ -1,85 +0,0 @@ -module Athena::Framework::CompilerPasses::RegisterEventListenersPass - include Athena::DependencyInjection::PreArgumentsCompilerPass - - macro included - macro finished - {% verbatim do %} - {% - SERVICE_HASH["event_dispatcher"][:factory] = {"self".id, "get_event_dispatcher"} - - TAG_HASH[ATH::Listeners::TAG].each do |service_id| - SERVICE_HASH[service_id][:visibility] = Visibility::INTERNAL - end - %} - - private def get_event_dispatcher - dispatcher = AED::EventDispatcher.new - - {% for service_id in TAG_HASH[ATH::Listeners::TAG] %} - {% metadata = SERVICE_HASH[service_id] %} - - {% listeners = [] of Nil %} - - # Changes made here should also be reflected within `AED::EventListenerInterface` overload within `AED::EventDispatcher`. - {% - class_listeners = metadata[:service].class.methods.select &.annotation(AEDA::AsEventListener) - - # Raise compile time error if a listener is defined as a class method. - unless class_listeners.empty? - class_listeners.first.raise "Event listener methods can only be defined as instance methods. Did you mean '#{metadata[:service].name}##{class_listeners.first.name}'?" - end - - metadata[:service].methods.select(&.annotation(AEDA::AsEventListener)).each do |m| - # Validate the parameters of each method. - if (m.args.size < 1) || (m.args.size > 2) - m.raise "Expected '#{metadata[:service].name}##{m.name}' to have 1..2 parameters, got '#{m.args.size}'." - end - - event_arg = m.args[0] - - # Validate the type restriction of the first parameter, if present - event_arg.raise "Expected parameter #1 of '#{metadata[:service].name}##{m.name}' to have a type restriction of an 'AED::Event' instance, but it is not restricted." if event_arg.restriction.is_a?(Nop) - event_arg.raise "Expected parameter #1 of '#{metadata[:service].name}##{m.name}' to have a type restriction of an 'AED::Event' instance, not '#{event_arg.restriction}'." if !(event_arg.restriction.resolve <= AED::Event) - - if dispatcher_arg = m.args[1] - event_arg.raise "Expected parameter #2 of '#{metadata[:service].name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', but it is not restricted." if dispatcher_arg.restriction.is_a?(Nop) - event_arg.raise "Expected parameter #2 of '#{metadata[:service].name}##{m.name}' to have a type restriction of 'AED::EventDispatcherInterface', not '#{dispatcher_arg.restriction}'." if !(dispatcher_arg.restriction.resolve <= AED::EventDispatcherInterface) - end - - priority = m.annotation(AEDA::AsEventListener)[:priority] || 0 - - unless priority.is_a? NumberLiteral - m.raise "Event listener method '#{metadata[:service].name}##{m.name}' expects a 'NumberLiteral' for its 'AEDA::AsEventListener#priority' field, but got a '#{priority.class_name.id}'." - end - - listeners << { - event_arg.restriction.resolve.id, - m.args.size, - m.name.id, - "#{metadata[:service]}##{m.name.id}", - priority, - } - end - %} - - {% for info in listeners %} - {% event, count, method, name, priority = info %} - - {% if 1 == count %} - dispatcher.add_callable( - {{event}}.callable(priority: {{priority}}, name: {{name}}) { |event| self.{{service_id.id}}.{{method}} event.as({{event}}) }, - ) - {% else %} - dispatcher.add_callable( - {{event}}.callable(priority: {{priority}}, name: {{name}}) { |event, dispatcher| self.{{service_id.id}}.{{method}} event.as({{event}}), dispatcher }, - ) - {% end %} - {% end %} - {% end %} - - dispatcher - end - {% end %} - end - end -end From b1486ea9688fd5eee9e5c50d0feeba663fa4a9b5 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Sat, 21 Jan 2023 14:49:39 -0500 Subject: [PATCH 2/4] add console ext to DI component --- .../dependency_injection/src/ext/console.cr | 199 ++++++++++++++++++ src/components/framework/src/ext/console.cr | 2 - .../framework/src/ext/console/application.cr | 21 -- .../ext/console/container_command_loader.cr | 32 --- .../src/ext/console/register_commands.cr | 133 ------------ 5 files changed, 199 insertions(+), 188 deletions(-) create mode 100644 src/components/dependency_injection/src/ext/console.cr delete mode 100644 src/components/framework/src/ext/console/application.cr delete mode 100644 src/components/framework/src/ext/console/container_command_loader.cr delete mode 100644 src/components/framework/src/ext/console/register_commands.cr diff --git a/src/components/dependency_injection/src/ext/console.cr b/src/components/dependency_injection/src/ext/console.cr new file mode 100644 index 000000000..c8c64b0fb --- /dev/null +++ b/src/components/dependency_injection/src/ext/console.cr @@ -0,0 +1,199 @@ +# TODO: Clean this up once https://github.com/crystal-lang/crystal/issues/12965 is resolved +{% skip_file unless @top_level.has_constant?("Athena") && Athena.has_constant?("Console") && Athena::Console.has_constant?("Command") %} + +ADI.auto_configure ACON::Command, {tags: [ADI::Console::Command::TAG]} + +module Athena::DependencyInjection::Console + # :nodoc: + module Command + TAG = "athena.console.command" + end + + @[ADI::Register(public: true, name: "athena_console_application")] + # Entrypoint for the `Athena::Console` integration. + # This service should be fetched via `ADI.container` within your console CLI file. + # + # Checkout the [external documentation](/components/console/) for more information. + class Application < ACON::Application + def initialize( + command_loader : ACON::Loader::Interface? = nil, + event_dipatcher : AED::EventDispatcherInterface? = nil, + eager_commands : Enumerable(ACON::Command)? = nil + ) + super "Athena", SemanticVersion.parse ATH::VERSION + + self.command_loader = command_loader + # TODO: set event dispatcher when that's implemented in the console component. + + eager_commands.try &.each do |cmd| + self.add cmd + end + end + end + + # :nodoc: + class ContainerCommandLoader + include Athena::Console::Loader::Interface + + @command_map : Hash(String, ACON::Command.class) + + def initialize( + @command_map : Hash(String, ACON::Command.class), + @loader : ADI::Console::ContainerCommandLoaderLocator + ); end + + # :inherit: + def get(name : String) : ACON::Command + if !self.has? name + raise ACON::Exceptions::CommandNotFound.new "Command '#{name}' does not exist." + end + + @loader.get @command_map[name] + end + + # :inherit: + def has?(name : String) : Bool + @command_map.has_key? name + end + + # :inherit: + def names : Array(String) + @command_map.keys + end + end + + # :nodoc: + module CompilerPasses::RegisterCommands + # Post arguments avoid dependency resolution + include Athena::DependencyInjection::PostArgumentsCompilerPass + + macro included + macro finished + {% verbatim do %} + {% + command_map = {} of Nil => Nil + command_refs = {} of Nil => Nil + + # Services that are not configured via the annotation so must be registered eagerly. + eager_service_ids = [] of Nil + + (TAG_HASH[ADI::Console::Command::TAG] || [] of Nil).each do |service_id| + metadata = SERVICE_HASH[service_id] + + # TODO: Any benefit in allowing commands to be configured via tags instead of the annotation? + + metadata[:visibility] = metadata[:visibility] != Visibility::PRIVATE ? metadata[:visibility] : Visibility::INTERNAL + + ann = metadata[:service].annotation ACONA::AsCommand + + if ann == nil + if metadata[:visibility] == Visibility::PRIVATE + SERVICE_HASH[public_service_id = "_#{service_id.id}_public"] = metadata + {visibility: Visibility::INTERNAL} + service_id = public_service_id + end + eager_service_ids << service_id.id + else + name = ann[0] || ann[:name] + + unless name + ann.raise "Console command '#{metadata[:service]}' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field." + end + + aliases = name.split '|' + aliases = aliases + (ann[:aliases] || [] of Nil) + + if ann[:hidden] && "" != aliases[0] + aliases.unshift "" + end + + command_name = aliases[0] + aliases = aliases[1..] + + if is_hidden = "" == command_name + command_name = aliases[0] + aliases = aliases[1..] + end + + command_map[command_name] = metadata[:service] + command_refs[metadata[:service]] = service_id + + aliases.each do |a| + command_map[a] = metadata[:service] + end + + SERVICE_HASH[lazy_service_id = "_#{service_id.id}_lazy"] = { + visibility: Visibility::INTERNAL, + service: "ACON::Commands::Lazy", + ivar_type: "ACON::Commands::Lazy", + tags: [] of Nil, + generics: [] of Nil, + arguments: [ + {value: command_name}, + {value: "#{aliases} of String".id}, + {value: ann[:description] || ""}, + {value: is_hidden}, + {value: "->{ #{service_id.id}.as(ACON::Command) }".id}, + ], + } + + command_refs[metadata[:service]] = lazy_service_id + end + end + + SERVICE_HASH[loader_id = "athena_console_command_loader_container"] = { + visibility: Visibility::INTERNAL, + service: "Athena::DependencyInjection::Console::ContainerCommandLoaderLocator", + ivar_type: "Athena::DependencyInjection::Console::ContainerCommandLoaderLocator", + tags: [] of Nil, + generics: [] of Nil, + arguments: [ + {value: "self".id}, + ], + } + + SERVICE_HASH[command_loader_service_id = "athena_console_command_loader"] = { + visibility: Visibility::PUBLIC, + alias: ACON::Loader::Interface, + service: Athena::DependencyInjection::Console::ContainerCommandLoader, + ivar_type: Athena::DependencyInjection::Console::ContainerCommandLoader, + tags: [] of Nil, + generics: [] of Nil, + arguments: [ + {value: "#{command_map} of String => ACON::Command.class".id}, + {value: loader_id.id}, + ], + } + + SERVICE_HASH["athena_console_application"][:arguments][0][:value] = command_loader_service_id.id + SERVICE_HASH["athena_console_application"][:arguments][2][:value] = "#{eager_service_ids} of ACON::Command".id + %} + + # :nodoc: + # + # TODO: Define some more generic way to create these + struct ::Athena::DependencyInjection::Console::ContainerCommandLoaderLocator + def initialize(@container : ::ADI::ServiceContainer); end + + {% for service_type, service_id in command_refs %} + def get(service : {{service_type}}.class) : ACON::Command + @container.{{service_id.id}} + end + {% end %} + + def get(service) : ACON::Command + {% begin %} + case service + {% for service_type, service_id in command_refs %} + when {{service_type}} then @container.{{service_id.id}} + {% end %} + else + raise "BUG: Couldn't find correct service." + end + {% end %} + end + end + {% end %} + end + end + end +end diff --git a/src/components/framework/src/ext/console.cr b/src/components/framework/src/ext/console.cr index c3f822940..383bbb4f2 100644 --- a/src/components/framework/src/ext/console.cr +++ b/src/components/framework/src/ext/console.cr @@ -1,3 +1 @@ require "./console/**" - -ADI.auto_configure ACON::Command, {tags: ["athena.console.command"]} diff --git a/src/components/framework/src/ext/console/application.cr b/src/components/framework/src/ext/console/application.cr deleted file mode 100644 index 294d43931..000000000 --- a/src/components/framework/src/ext/console/application.cr +++ /dev/null @@ -1,21 +0,0 @@ -@[ADI::Register(public: true, name: "athena_console_application")] -# Entrypoint for the `Athena::Console` integration. -# This service should be fetched via `ADI.container` within your console CLI file. -# -# Checkout the [external documentation](/components/console/) for more information. -class Athena::Framework::Console::Application < ACON::Application - def initialize( - command_loader : ACON::Loader::Interface? = nil, - event_dipatcher : AED::EventDispatcherInterface? = nil, - eager_commands : Enumerable(ACON::Command)? = nil - ) - super "Athena", SemanticVersion.parse ATH::VERSION - - self.command_loader = command_loader - # TODO: set event dispatcher when that's implemented in the console component. - - eager_commands.try &.each do |cmd| - self.add cmd - end - end -end diff --git a/src/components/framework/src/ext/console/container_command_loader.cr b/src/components/framework/src/ext/console/container_command_loader.cr deleted file mode 100644 index a3cfac9d2..000000000 --- a/src/components/framework/src/ext/console/container_command_loader.cr +++ /dev/null @@ -1,32 +0,0 @@ -# :nodoc: -# -# This service is wired up manually -class Athena::Framework::Console::ContainerCommandLoader - include Athena::Console::Loader::Interface - - @command_map : Hash(String, ACON::Command.class) - - def initialize( - @command_map : Hash(String, ACON::Command.class), - @loader : ATH::Console::ContainerCommandLoaderLocator - ); end - - # :inherit: - def get(name : String) : ACON::Command - if !self.has? name - raise ACON::Exceptions::CommandNotFound.new "Command '#{name}' does not exist." - end - - @loader.get @command_map[name] - end - - # :inherit: - def has?(name : String) : Bool - @command_map.has_key? name - end - - # :inherit: - def names : Array(String) - @command_map.keys - end -end diff --git a/src/components/framework/src/ext/console/register_commands.cr b/src/components/framework/src/ext/console/register_commands.cr deleted file mode 100644 index aa13f3f72..000000000 --- a/src/components/framework/src/ext/console/register_commands.cr +++ /dev/null @@ -1,133 +0,0 @@ -module Athena::Framework::CompilerPasses::RegisterCommands - # Post arguments avoid dependency resolution - include Athena::DependencyInjection::PostArgumentsCompilerPass - - macro included - macro finished - {% verbatim do %} - {% - command_map = {} of Nil => Nil - command_refs = {} of Nil => Nil - - # Services that are not configured via the annotation so must be registered eagerly. - eager_service_ids = [] of Nil - - (TAG_HASH["athena.console.command"] || [] of Nil).each do |service_id| - metadata = SERVICE_HASH[service_id] - - # TODO: Any benefit in allowing commands to be configured via tags instead of the annotation? - - metadata[:visibility] = metadata[:visibility] != Visibility::PRIVATE ? metadata[:visibility] : Visibility::INTERNAL - - ann = metadata[:service].annotation ACONA::AsCommand - - if ann == nil - if metadata[:visibility] == Visibility::PRIVATE - SERVICE_HASH[public_service_id = "_#{service_id.id}_public"] = metadata + {visibility: Visibility::INTERNAL} - service_id = public_service_id - end - eager_service_ids << service_id.id - else - name = ann[0] || ann[:name] - - unless name - ann.raise "Console command '#{metadata[:service]}' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field." - end - - aliases = name.split '|' - aliases = aliases + (ann[:aliases] || [] of Nil) - - if ann[:hidden] && "" != aliases[0] - aliases.unshift "" - end - - command_name = aliases[0] - aliases = aliases[1..] - - if is_hidden = "" == command_name - command_name = aliases[0] - aliases = aliases[1..] - end - - command_map[command_name] = metadata[:service] - command_refs[metadata[:service]] = service_id - - aliases.each do |a| - command_map[a] = metadata[:service] - end - - SERVICE_HASH[lazy_service_id = "_#{service_id.id}_lazy"] = { - visibility: Visibility::INTERNAL, - service: "ACON::Commands::Lazy", - ivar_type: "ACON::Commands::Lazy", - tags: [] of Nil, - generics: [] of Nil, - arguments: [ - {value: command_name}, - {value: "#{aliases} of String".id}, - {value: ann[:description] || ""}, - {value: is_hidden}, - {value: "->{ #{service_id.id}.as(ACON::Command) }".id}, - ], - } - - command_refs[metadata[:service]] = lazy_service_id - end - end - - SERVICE_HASH[loader_id = "athena_console_command_loader_container"] = { - visibility: Visibility::INTERNAL, - service: "Athena::Framework::Console::ContainerCommandLoaderLocator", - ivar_type: "Athena::Framework::Console::ContainerCommandLoaderLocator", - tags: [] of Nil, - generics: [] of Nil, - arguments: [ - {value: "self".id}, - ], - } - - SERVICE_HASH[command_loader_service_id = "athena_console_command_loader"] = { - visibility: Visibility::PUBLIC, - alias: ACON::Loader::Interface, - service: Athena::Framework::Console::ContainerCommandLoader, - ivar_type: Athena::Framework::Console::ContainerCommandLoader, - tags: [] of Nil, - generics: [] of Nil, - arguments: [ - {value: "#{command_map} of String => ACON::Command.class".id}, - {value: loader_id.id}, - ], - } - - SERVICE_HASH["athena_console_application"][:arguments][0][:value] = command_loader_service_id.id - SERVICE_HASH["athena_console_application"][:arguments][2][:value] = "#{eager_service_ids} of ACON::Command".id - %} - - # :nodoc: - # - # TODO: Define some more generic way to create these - struct ::Athena::Framework::Console::ContainerCommandLoaderLocator - def initialize(@container : ::ADI::ServiceContainer); end - - {% for service_type, service_id in command_refs %} - def get(service : {{service_type}}.class) : ACON::Command - @container.{{service_id.id}} - end - {% end %} - - def get(service) : ACON::Command - {% begin %} - case service - {% for service_type, service_id in command_refs %} - when {{service_type}} then @container.{{service_id.id}} - {% end %} - else - raise "BUG: Couldn't find correct service." - end - {% end %} - end - end - {% end %} - end - end -end From e0bc3d73780f9f73b16c2b7b9ee5ccb0d6b6b0c4 Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Fri, 3 Feb 2023 21:27:38 -0500 Subject: [PATCH 3/4] Update compiler pass to new type --- src/components/event_dispatcher/src/event_dispatcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/event_dispatcher/src/event_dispatcher.cr b/src/components/event_dispatcher/src/event_dispatcher.cr index 7d10f5f6e..67a55e854 100644 --- a/src/components/event_dispatcher/src/event_dispatcher.cr +++ b/src/components/event_dispatcher/src/event_dispatcher.cr @@ -55,7 +55,7 @@ class Athena::EventDispatcher::EventDispatcher {% begin %} {% listeners = [] of Nil %} - # Changes made here should also be reflected within `ATH::CompilerPasses::RegisterEventListenersPass`. + # Changes made here should also be reflected within `ADI::EventDispatcher::CompilerPasses::RegisterEventListenersPass`. {% class_listeners = T.class.methods.select &.annotation(AEDA::AsEventListener) From ab959572a3c4512a27dc621a4715c3d5184794ae Mon Sep 17 00:00:00 2001 From: George Dietrich Date: Fri, 3 Feb 2023 21:47:54 -0500 Subject: [PATCH 4/4] Update console and event dispatcher components to note ADI integration --- src/components/console/src/athena-console.cr | 3 +++ .../dependency_injection/src/ext/console.cr | 13 ++++++++++++- .../event_dispatcher/src/athena-event_dispatcher.cr | 3 +++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/console/src/athena-console.cr b/src/components/console/src/athena-console.cr index f9ab6cbfd..6a548ebdf 100644 --- a/src/components/console/src/athena-console.cr +++ b/src/components/console/src/athena-console.cr @@ -90,6 +90,9 @@ alias ACONA = ACON::Annotations # # From here you can then setup your entry point file talked about earlier, being sure to require the component via `require "athena-console"`. # Finally, create/require your `ACON::Command`s, and customize the `ACON::Application` as needed. +# +# TIP: If using this component with the `Athena::DependencyInjection` component, `ACON::Command` that have the `ADI::Register` annotation will automatically +# be registered as commands when using the `ADI::Console::Application` type. module Athena::Console VERSION = "0.3.0" diff --git a/src/components/dependency_injection/src/ext/console.cr b/src/components/dependency_injection/src/ext/console.cr index c8c64b0fb..99eeb25fd 100644 --- a/src/components/dependency_injection/src/ext/console.cr +++ b/src/components/dependency_injection/src/ext/console.cr @@ -3,6 +3,7 @@ ADI.auto_configure ACON::Command, {tags: [ADI::Console::Command::TAG]} +# Contains types related to the `Athena::Console` integration. module Athena::DependencyInjection::Console # :nodoc: module Command @@ -13,9 +14,17 @@ module Athena::DependencyInjection::Console # Entrypoint for the `Athena::Console` integration. # This service should be fetched via `ADI.container` within your console CLI file. # + # ``` + # # Require your code + # require "./main" + # + # # Run the application + # ADI.container.athena_console_application.run + # ``` + # # Checkout the [external documentation](/components/console/) for more information. class Application < ACON::Application - def initialize( + protected def initialize( command_loader : ACON::Loader::Interface? = nil, event_dipatcher : AED::EventDispatcherInterface? = nil, eager_commands : Enumerable(ACON::Command)? = nil @@ -63,6 +72,8 @@ module Athena::DependencyInjection::Console end # :nodoc: + module CompilerPasses; end + module CompilerPasses::RegisterCommands # Post arguments avoid dependency resolution include Athena::DependencyInjection::PostArgumentsCompilerPass diff --git a/src/components/event_dispatcher/src/athena-event_dispatcher.cr b/src/components/event_dispatcher/src/athena-event_dispatcher.cr index 685228d14..fc11772d5 100644 --- a/src/components/event_dispatcher/src/athena-event_dispatcher.cr +++ b/src/components/event_dispatcher/src/athena-event_dispatcher.cr @@ -92,6 +92,9 @@ alias AEDA = AED::Annotations # WARNING: If using this component within the context of something that handles independent execution flows, such as a web framework, you will want there to be a dedicated dispatcher instance for each path. # This ensures that one flow will not leak state to any other flow, while still allowing flow specific mutations to be used. # Consider pairing this component with the [Athena::DependencyInjection][Athena::DependencyInjection--getting-started] component as a way to handle this. +# +# TIP: If using this component with the `Athena::DependencyInjection` component, `AED::EventListenerInterface` that have the `ADI::Register` annotation will automatically +# be registered with the default `AED::EventDispatcherInterface`. module Athena::EventDispatcher VERSION = "0.2.0"