Skip to content

Instantly share code, notes, and snippets.

@jpinnix
Forked from minhajuddin/config_compiler.exs
Created August 23, 2018 13:47
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 jpinnix/37d6bc3093a7be0f6c71554a671463c3 to your computer and use it in GitHub Desktop.
Save jpinnix/37d6bc3093a7be0f6c71554a671463c3 to your computer and use it in GitHub Desktop.
Compiled configuration
# This is the target module which will be overwritten after dynamic compilation
# You'll be using this to read configuration in your code. For instance, if you
# have a configuration key called `:redis_timeout`, you could read it using
# `MM.Config.get(:redis_timeout)`
defmodule MM.Config do
# we use a default implementation which raises an error when our code tries
# to read configuration before it is compiled.
def get(_key), do: raise("Config has not been compiled yet!")
end
defmodule MM.ConfigCompiler do
@moduledoc """
Compiles essential settings into a dynamic module
This allows us to read these settings very fast.
The sole purpose of this module is to take a dynamic module and a keyword list
and compile it so that the dynamic module returns values of the keyword list
"""
@doc """
Compiles a new module embedding the keyword list as functions.
# Example
iex> MM.ConfigCompiler.compile(MM.Config, [redis_timeout: 1000, host: "192.168.1.1"])
iex> MM.Config.get(:redis_timeout)
1000
iex> MM.Config.get(:non_existent_key)
** (RuntimeError) CONFIG_NOT_FOUND: non_existent_key
"""
def compile(module_name, config) do
# the compile module creates a new module using quote in conjunction with
# Code.eval_quoted
# we first create a quote containing the module definition
# We need `Macro.escape` to escape complex elixir types like maps when used inside quote
# We also use `location: :keep` to show us the file where this is being done when an error is raised
quote bind_quoted: [config: Macro.escape(config), module_name: module_name],
location: :keep do
# define our module
defmodule module_name do
# for each key value pair in the input keyword list
for {k, v} <- config do
# define a function head matching the literal key and return the literal value
# e.g. def get(:redis_timeout), do: 1000
def get(unquote(k)), do: unquote(v)
end
# if the input key doesn't match any of the previous function heads, it falls down
# to this default callback where we raise an exception
def get(any), do: raise("CONFIG_NOT_FOUND: #{inspect(any)}")
end
end
# We have the whole quoted module at this point and we just push it into
# Code.eval_quoted to compile it.
|> Code.eval_quoted([], __ENV__)
end
end
# This is a helper module which encapsulates the reading and compiling of configuration
# so that it can be called without a lot of ceremony
defmodule MM.ConfigHelper do
def recompile do
config = Application.get_all_env(:my_app)
# => Returns something like below
# [
# {:idle_timeout_ms, 120000},
# {:statsd_tags, ["host:evtp", "app_version:0.1.0", "env:dev"]},
# {:namespace, MM},
# # ...
# ]
# Overwrite the current module with a new definition containing function
# heads for the config values.
MM.Config.compile(MM.Config, config)
end
end
# in your application.ex and whenever you want to update env/recompile
MM.ConfigHelper.recompile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment