Forked from christhekeele/1-indirect_uses_tracker.ex
Created
December 18, 2017 04:49
-
-
Save retgoat/817bb2c027beea40f524f919b73f3401 to your computer and use it in GitHub Desktop.
A way to track when modules are used in Elixir, and an example adapter/plugin architecture built on top.
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
# For simpler use cases, see the UsesTracker instead: | |
# https://gist.github.com/christhekeele/e858881d0ca2053295c6e10d8692e6ea | |
### | |
# A way to know, at runtime, what modules a module has used at compile time. | |
# In this case, you include `IndirectUsesTracker` into a module. When that module gets | |
# used in some other module, it makes that module registerable under a namespace of your choosing. | |
# When the registerable module is used into a third module, that third module will know at runtime which | |
# registerables were `use`d in it at compile time, via a function titled after the namespace. | |
# TLDR; when used, makes any users of the using module aggregate it's uses of users of this module in turn... | |
# It's a little clearer in the use-case below. | |
## | |
defmodule IndirectUsesTracker do | |
defmodule Registry do | |
defmacro __before_compile__(_) do | |
quote do | |
def unquote(Module.get_attribute(__CALLER__.module, :registry_name))() do | |
unquote(Module.get_attribute(__CALLER__.module, Module.get_attribute(__CALLER__.module, :registry_name))) | |
end | |
end | |
end | |
end | |
defmacro __using__(name) do | |
quote do | |
defmacro __using__(_) do | |
name = unquote(name) | |
quote do | |
@before_compile Registry | |
@registry_name unquote(name) | |
Module.register_attribute(__MODULE__, @registry_name, accumulate: true) | |
Module.put_attribute(__MODULE__, @registry_name, unquote(__MODULE__)) | |
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
### | |
# Let's use this to make a plugin system. | |
# Users of our library will be able to define plugins at compile-time, | |
# extend their default behaviour, and use them in their application, | |
# and at runtime they can act on which of their own plugins were used | |
# in their own code. | |
## | |
### | |
# Our end users will 'use' this to define a new plugin. | |
## | |
defmodule Library.Plugin.Behaviour do | |
defmacro __using__(_) do | |
quote location: :keep do | |
use IndirectUsesTracker, :plugins | |
# Your code here... | |
use GenServer | |
def start_link(opts \\ []) do | |
GenServer.start_link(__MODULE__, opts) | |
end | |
defoverridable start_link: 0 | |
defoverridable start_link: 1 | |
def init(opts) do | |
{ :ok, opts } | |
end | |
defoverridable init: 1 | |
end | |
end | |
@callback start_link :: GenServer.on_start | |
@callback start_link(Keyword.t) :: GenServer.on_start | |
@callback init(args :: term) :: | |
{:ok, state} | | |
{:ok, state, timeout | :hibernate} | | |
:ignore | | |
{:stop, reason :: any} when state: any | |
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
### | |
# Here's an example of how a user might consume our plugin system: | |
## | |
### | |
# They can use our base plugin behavior to get whatever | |
# default functionality we want for them. | |
## | |
defmodule Client.MyPlugin do | |
use Library.Plugin.Behaviour | |
# Their code here... | |
def init(opts) do | |
IO.puts "Started #{__MODULE__} with opts:" | |
IO.inspect opts | |
{ :ok, opts } | |
end | |
end | |
### | |
# They can override any behaviour we defined, too. | |
## | |
defmodule Client.MyOtherPlugin do | |
use Library.Plugin.Behaviour | |
# Their code here... | |
def init(opts) do | |
IO.puts "Started #{__MODULE__} with opts:" | |
IO.inspect opts | |
{ :ok, opts } | |
end | |
end | |
### | |
# For good measure. | |
## | |
defmodule Client.UnusedPlugin do | |
use Library.Plugin.Behaviour | |
end | |
### | |
# When they use those plugins in a module like this, the module will know | |
# at runtime which ones the used and act on them. The only caveat to this is that | |
# currently a single module may not use modules from different namespaces: | |
# we couldn't `use IndirectUsesTracker, :hooks` and put a hook in | |
# Client.Plugin.Supervisor as well. | |
## | |
defmodule Client.Plugin.Supervisor do | |
use Supervisor | |
use Client.MyPlugin | |
use Client.MyOtherPlugin | |
def start_link(opts \\ []) do | |
Supervisor.start_link(__MODULE__, opts) | |
end | |
### | |
# THIS IS THE WHOLE POINT: | |
# Here our 'registry module'--this supervisor--has access at runtime | |
# to the plugins it `use`d at compile time, under the namespace | |
# we originally passed into the macro inside the base plugin: `:plugins`. | |
## | |
def init(opts) do | |
children = Enum.map(plugins, fn plugin -> | |
worker(plugin, [opts]) | |
] | |
supervise(children, strategy: :one_for_one) | |
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
### | |
# Proof of runtime access. | |
## | |
Client.Plugin.Supervisor.plugins # => [Client.MyOtherPlugin, Client.MyPlugin] | |
### | |
# Notice that they're always in reverse order added, if order mattered we could | |
# could `Enum.reverse(plugins)` before consumption. | |
## | |
### | |
# In action: starting the supervision tree for our registry module at runtime | |
# adds the plugins we gave it at compile time. | |
# Notice: no `UnusedPlugin` appears, just the ones we actually `use`d. | |
## | |
Client.Plugin.Supervisor.start_link(foo: :bar, fizz: :buzz) | |
# Started Elixir.Client.MyOtherPlugin with opts: | |
# [foo: :bar, fizz: :buzz] | |
# Started Elixir.Client.MyPlugin with opts: | |
# [foo: :bar, fizz: :buzz] | |
### | |
# Anything that `use`s a plugin will be able to tell which--you could | |
# make supervisors that use different plugins and they'd spin up | |
# their own workers, for instance, or you could have the registry | |
# pass its plugins back into the library and have the library start | |
# a complicated supervision tree for you, as a part of its | |
# application boot process you define in your mix.exs. | |
# | |
# FIN! | |
## |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment