Skip to content

Instantly share code, notes, and snippets.

@bitwalker
Created July 19, 2016 23:00
Show Gist options
  • Star 65 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save bitwalker/a4f73b33aea43951fe19b242d06da7b9 to your computer and use it in GitHub Desktop.
Save bitwalker/a4f73b33aea43951fe19b242d06da7b9 to your computer and use it in GitHub Desktop.
Useful config wrapper for Elixir
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
@bitwalker
Copy link
Author

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!

@tomhouman
Copy link

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?

@linjunpop
Copy link

Maybe this should be a feature of Application.get_env/3, considering submit a PR to elixir-lang/elixir ?

@BennyHallett
Copy link

👍 For getting this change into elixir-lang/elixir

@edevil
Copy link

edevil commented Sep 15, 2016

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?

@st23am
Copy link

st23am commented Sep 27, 2016

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/

@whitfin
Copy link

whitfin commented Sep 29, 2016

@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.

@zweizeichen
Copy link

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?

@gmodarelli
Copy link

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

@shiroyasha
Copy link

@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.

@cdegroot
Copy link

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.

@almassapargali
Copy link

almassapargali commented Mar 1, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment