-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
More tightly integrate DI component with the other components (#259)
* Add event dispatcher ext to DI component * Add console ext to DI component
- Loading branch information
1 parent
0ad460e
commit 52e3796
Showing
13 changed files
with
325 additions
and
287 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
# 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]} | ||
|
||
# Contains types related to the `Athena::Console` integration. | ||
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. | ||
# | ||
# ``` | ||
# # 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 | ||
protected 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; end | ||
|
||
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 |
101 changes: 101 additions & 0 deletions
101
src/components/dependency_injection/src/ext/event_dispatcher.cr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1 @@ | ||
require "./console/**" | ||
|
||
ADI.auto_configure ACON::Command, {tags: ["athena.console.command"]} |
Oops, something went wrong.