Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save retgoat/817bb2c027beea40f524f919b73f3401 to your computer and use it in GitHub Desktop.
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.
# 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
###
# 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
###
# 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
###
# 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