-
-
Save bitwalker/a4f73b33aea43951fe19b242d06da7b9 to your computer and use it in GitHub Desktop.
defmodule Config do | |
@moduledoc """ | |
This module handles fetching values from the config with some additional niceties | |
""" | |
@doc """ | |
Fetches a value from the config, or from the environment if {:system, "VAR"} | |
is provided. | |
An optional default value can be provided if desired. | |
## Example | |
iex> {test_var, expected_value} = System.get_env |> Enum.take(1) |> List.first | |
...> Application.put_env(:myapp, :test_var, {:system, test_var}) | |
...> ^expected_value = #{__MODULE__}.get(:myapp, :test_var) | |
...> :ok | |
:ok | |
iex> Application.put_env(:myapp, :test_var2, 1) | |
...> 1 = #{__MODULE__}.get(:myapp, :test_var2) | |
1 | |
iex> :default = #{__MODULE__}.get(:myapp, :missing_var, :default) | |
:default | |
""" | |
@spec get(atom, atom, term | nil) :: term | |
def get(app, key, default \\ nil) when is_atom(app) and is_atom(key) do | |
case Application.get_env(app, key) do | |
{:system, env_var} -> | |
case System.get_env(env_var) do | |
nil -> default | |
val -> val | |
end | |
{:system, env_var, preconfigured_default} -> | |
case System.get_env(env_var) do | |
nil -> preconfigured_default | |
val -> val | |
end | |
nil -> | |
default | |
val -> | |
val | |
end | |
end | |
@doc """ | |
Same as get/3, but returns the result as an integer. | |
If the value cannot be converted to an integer, the | |
default is returned instead. | |
""" | |
@spec get_integer(atom(), atom(), integer()) :: integer | |
def get_integer(app, key, default \\ nil) do | |
case get(app, key, nil) do | |
nil -> default | |
n when is_integer(n) -> n | |
n -> | |
case Integer.parse(n) do | |
{i, _} -> i | |
:error -> default | |
end | |
end | |
end | |
end |
I'd like to use this in order to support configuring a dynamic host at runtime for my phoenix app, which is built using distillery. For example, I'd like to be able to do this in my config/prod.exs:
config :my_app, MyApp.Endpoint,
http: [port: {:system, "PORT"}],
url: [host: {:system, "MYAPP_HOST"}]
It looks like your module is exactly what I need, but I'm not sure how to pull it into my project. Can I use the syntax as above, or do I need to use Config.get explicitly? Is this even going to work inside a mix config file?
Maybe this should be a feature of Application.get_env/3
, considering submit a PR to elixir-lang/elixir ?
👍 For getting this change into elixir-lang/elixir
Does System.get_env/1 imply a getenv() system call? If so, isn't that a performance problem if you're doing it in a function that's called frequently?
When I try and use this in my umbrella project and import into my config.exs * (Mix.Config.LoadError) could not load config config/config.exs ** (UndefinedFunctionError) function MobileApi.ConfigurationUtils.get/3 is undefined (module MobileApi.ConfigurationUtils is not available)
How should I use this? Is there some strange umbrella thing going on here? I put the file in apps/mobile_api/lib/mobile_api/
@st23am You don't call it from your config.exs file, you call it at runtime. Your config doesn't change, this just allows overriding at runtime.
FWIW I also believe this should be in core, seems a good thing to make standard.
Seems reasonable to have this in core, configurations like this are part of many production deployments and a continuous source of confusion due to custom hand-rolled solutions. Having a standardised solution would simplify these things a lot (especially if it gains wider adoption in libs). @bitwalker what do you think about opening an issue/PR in elixir-lang/elixir?
Thank you very much @bitwalker. I've added a third function get!/2
to mimic the Application.fetch_env!/2
behavior.
https://gist.github.com/gmodarelli/946235798a1678c0ac31a3a8fcabdf16
@bitwalker awesome gist! 👏
I noticed that we share a similar boilerplate code on a lot of our projects.
To avoid copy/paste errors, I've tried to to wrap it into a installable dependancy.
I hope someone else will find it useful as well: https://github.com/renderedtext/ex-config.
I also encourage @bitwalker to open a PR for elixirlang. I would really like it if this would be the default in elixir.
FWIW - I'd prefer to see this land in the equivalent Erlang code (http://erlang.org/doc/apps/kernel/application.html#get_env-1) which should enable 12FA-style config of any app, whether it's running Elixir or Erlang modules.
I often group configs with common domain, so I also had to get nested config values, use following code:
def get_config(app, path, default \\ nil) do
[root | subkeys] = List.wrap(path)
root_val = Application.get_env(app, root)
val = Enum.reduce(subkeys, root_val, fn (subkey, val) ->
case val do
nil -> nil
keyword when is_list(keyword) -> Keyword.get(keyword, subkey)
map when is_map(map) -> Map.get(map, subkey)
end
end)
get_config_val(val || default)
end
def get_config_val({:system, varname}), do: System.get_env(varname)
def get_config_val(val), do: val
So, to get :domain
in this config:
config :my_app, MyApp.MailingService.Client,
api_key: {:system, "MAILGUN_API_KEY"},
domain: {:system, "MAILGUN_DOMAIN"}
I'd do following inside MyApp.MailingService.Client
:
def process_url(path) do
"https://api.mailgun.net/v3/" <> get_config(:my_app, [__MODULE__, :domain]) <> path
end
In case anyone is curious about licensing of this code, it shares the same license as pretty much all of my open source projects, the MIT license. Feel free to use as you see fit!