Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 54 additions & 47 deletions lib/deferred_config.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule DeferredConfig do
defmodule DeferredConfig do
@moduledoc """
Seamlessly add runtime config to your library, with the
"system tuples" or the `{m,f,a}` patterns.
Expand All @@ -15,27 +15,27 @@ defmodule DeferredConfig do
end

Where `:mine` is the name of your OTP app.

Now you and users of your app or lib can configure
as follows, and it'll work -- regardless of if they're
running it from iex, or a release with env vars set:
config :mine,

config :mine,

# string from env var, or `nil` if missing.
port1: {:system, "PORT"},

# string from env var |> integer; `nil` if missing.
port2: {:system, "PORT", {String, :to_integer}},

# string from env var, or "4000" as default.
port3: {:system, "PORT", "4000"},

# converts env var to integer, or 4000 as default.
port4: {:system, "PORT", 4000, {String, :to_integer}}

**Accessing config does not change.**

Since you can use arbitrary transformation functions,
you can do advanced transformations if you need to:

Expand All @@ -50,11 +50,11 @@ defmodule DeferredConfig do
end
end

# config.exs
# config.exs
config :my_app,
port: {:system, "MY_IP", {127,0,0,1}, {Mine.Ip, :str2ip}

See `README.md` for explanation of rationale.
See `README.md` for explanation of rationale.
**TL;DR:** `REPLACE_OS_VARS` is string-only and release-only,
and `{:system, ...}` support among libraries is spotty
and easy to get wrong in ways that bite your users
Expand All @@ -65,19 +65,17 @@ defmodule DeferredConfig do
"""
require Logger
import ReplacingWalk, only: [walk: 3]

@default_rts [
{&DeferredConfig.recognize_system_tuple/1,
&DeferredConfig.get_system_tuple/1},
{&DeferredConfig.recognize_mfa_tuple/1,
&DeferredConfig.transform_mfa_tuple/1}
{&DeferredConfig.recognize_system_tuple/1, &DeferredConfig.get_system_tuple/1},
{&DeferredConfig.recognize_mfa_tuple/1, &DeferredConfig.transform_mfa_tuple/1}
]

@doc """
Populate deferred values in an app's config.
Best run during `Application.start/2`.

**By default** attempts to populate the common
**By default** attempts to populate the common
`{:system, "VAR"}` tuple form for getting values from
`System.get_env/1`, and the more
general `{:apply, {Mod, fun, [args]}}` form as well.
Expand All @@ -86,33 +84,34 @@ defmodule DeferredConfig do
defaults and conversion functions, see
`Peerage.DeferredConfig.get_system_tuple/1`.

Can be extended by passing in a different
enumerable of `{&recognizer/1, &transformer/1}`
Can be extended by passing in a different
enumerable of `{&recognizer/1, &transformer/1}`
functions.
"""
def populate(app, transforms \\ @default_rts) do
:ok = app
|> Application.get_all_env
|> transform_cfg(transforms)
|> apply_transformed_cfg!(app)
:ok =
app
|> Application.get_all_env()
|> transform_cfg(transforms)
|> apply_transformed_cfg!(app)
end

@doc """
Given a config kvlist, and an enumerable of
Given a config kvlist, and an enumerable of
`{&recognize/1, &transform/1}` functions,
returns a kvlist with the values transformed
via replacing walk.
"""
def transform_cfg(cfg, rts \\ @default_rts) when is_list(rts) do
Enum.map(cfg, fn {k,v} ->
Enum.map(cfg, fn {k, v} ->
{k, apply_rts(v, rts)}
end)
end

@doc "`Application.put_env/3` for config kvlist"
def apply_transformed_cfg!(kvlist, app) do
kvlist
|> Enum.each(fn {k,v} ->
|> Enum.each(fn {k, v} ->
Application.put_env(app, k, v)
end)
end
Expand All @@ -126,6 +125,7 @@ defmodule DeferredConfig do

# apply sequence of replacing walks to a value
defp apply_rts(val, []), do: val

defp apply_rts(val, rts) when is_list(rts) do
Enum.reduce(rts, val, fn {r, t}, acc_v ->
walk(acc_v, r, t)
Expand All @@ -136,20 +136,24 @@ defmodule DeferredConfig do
Recognize mfa tuple, like `{:apply, {File, :read!, ["name"]}}`.
Returns `true` on recognition, `false` otherwise.
"""
def recognize_mfa_tuple({:apply, {m,f,a}})
when is_atom(m) and is_atom(f) and is_list(a),
do: true
def recognize_mfa_tuple({:apply, {m, f, a}})
when is_atom(m) and is_atom(f) and is_list(a),
do: true

def recognize_mfa_tuple({:apply, t}) do
Logger.error "badcfg - :apply needs {:m, :f, lst}. "<>
"given: #{ inspect t }"
Logger.error(
"badcfg - :apply needs {:m, :f, lst}. " <>
"given: #{inspect(t)}"
)

false
end

def recognize_mfa_tuple(_), do: false

@doc "Return evaluated `{:apply, {mod, fun, args}}` tuple."
def transform_mfa_tuple({:apply, {m,f,a}}), do: apply(m,f,a)
def transform_mfa_tuple({:apply, {m, f, a}}), do: apply(m, f, a)


@doc """
Recognizer for system tuples of forms:
- `{:system, "VAR"}`
Expand All @@ -158,28 +162,31 @@ defmodule DeferredConfig do
- `{:system, "VAR", default_value, {String, :to_integer}}`
Returns `true` when it matches one, `false` otherwise.
"""
def recognize_system_tuple({:system, ""<>_k}), do: true
def recognize_system_tuple({:system, ""<>_k, _default}), do: true
def recognize_system_tuple({:system, ""<>_k, _d, _mf}), do: true
def recognize_system_tuple(_), do: false
def recognize_system_tuple({:system, "" <> _k}), do: true
def recognize_system_tuple({:system, "" <> _k, _default}), do: true
def recognize_system_tuple({:system, "" <> _k, _d, _mf}), do: true
def recognize_system_tuple(_), do: false

@doc """
Return transformed copy of recognized system tuples:
gets from env, optionally converts it, with
gets from env, optionally converts it, with
optional default if env returned nothing.
"""
def get_system_tuple({:system, k}), do: System.get_env(k)

def get_system_tuple({:system, k, {m, f}}) do
apply m, f, [ System.get_env(k) ]
apply(m, f, [System.get_env(k)])
end

def get_system_tuple({:system, k, d}), do: System.get_env(k) || d

def get_system_tuple({:system, k, d, {m, f}}) do
(val = System.get_env k) && apply(m, f, [val]) || d
# (val = System.get_env k) && apply(m, f, [val]) || d
case System.get_env(k) do
nil -> d
val -> apply(m, f, [val])
end
end
def get_system_tuple(t), do: throw "Could not fetch: #{inspect t}"




def get_system_tuple(t), do: throw("Could not fetch: #{inspect(t)}")
end

10 changes: 6 additions & 4 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []},
"credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]},
"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []},
"ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}}
%{
"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], [], "hexpm", "4fb7b2f7b04af13cf210b132f8d10db52d4a57d36cb974e8025d7fdb12ca97fc"},
"credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm", "90bca5180fe64c47343969ad3e32bf1edc18d929689e8c00c810655a7a391427"},
"earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], [], "hexpm", "0fdcd651f9689e81cda24c8e5d06947c5aca69dbd8ce3d836b02bcd0c6004592"},
"ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "5c30e436a5acfdc2fd8fe6866585fcaf30f434c611d8119d4f3390ced2a550f3"},
}
73 changes: 46 additions & 27 deletions test/deferred_config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,30 @@ defmodule DeferredConfigTest do
doctest DeferredConfig

@app :lazy_cfg_test_appname

defmodule MyMod do
def get_my_key(""<>bin), do: "your key is 1234. write it down."
def get_my_key("" <> bin), do: "your key is 1234. write it down."
end

setup do
delete_all_env(@app)
# give each test a fake env that looks like this
env = %{"PORT" => "4000"}

system_transform = fn
{:system, k} -> Map.get(env, k)
{:system, k, {m, f}} -> apply m, f, [Map.get(env, k)]
{:system, k, d} -> Map.get(env, k, d)
{:system, k, d, {m, f}} -> apply( m, f, [Map.get(env, k)]) || d
{:system, k} -> Map.get(env, k)
{:system, k, {m, f}} -> apply(m, f, [Map.get(env, k)])
{:system, k, d} -> Map.get(env, k, d)
{:system, k, d, {m, f}} -> apply(m, f, [Map.get(env, k)]) || d
end

# our mock stack -- only changes env var retrieval
transforms = [
{&DeferredConfig.recognize_system_tuple/1, system_transform},
{&DeferredConfig.recognize_mfa_tuple/1,
&DeferredConfig.transform_mfa_tuple/1}
{&DeferredConfig.recognize_mfa_tuple/1, &DeferredConfig.transform_mfa_tuple/1}
]
[transforms: transforms,
system_transform: system_transform]

[transforms: transforms, system_transform: system_transform]
end

test "system tuples support", %{system_transform: transform} do
Expand All @@ -34,20 +35,23 @@ defmodule DeferredConfigTest do
port2: {:system, "PORT", "1111"},
port3: {:system, "FAIL", "1111"},
port4: {:system, "PORT", {String, :to_integer}},
port5: [{:system, "PORT", 3000, {String, :to_integer}}],
port5: [{:system, "PORT", 3000, {String, :to_integer}}]
]
actual = cfg
|> DeferredConfig.transform_cfg([
{&DeferredConfig.recognize_system_tuple/1, transform}
])

actual =
cfg
|> DeferredConfig.transform_cfg([
{&DeferredConfig.recognize_system_tuple/1, transform}
])

assert actual[:port1] == "4000"
assert actual[:port2] == "4000"
assert actual[:port3] == "1111"
assert actual[:port4] == 4000
assert actual[:port5] == [4000]
actual |> DeferredConfig.apply_transformed_cfg!(@app)

actual = Application.get_all_env @app
actual = Application.get_all_env(@app)
assert actual[:port1] == "4000"
assert actual[:port2] == "4000"
assert actual[:port3] == "1111"
Expand All @@ -56,31 +60,46 @@ defmodule DeferredConfigTest do
end

test "non-existent tuple values are handled" do
r = DeferredConfig.transform_cfg([key: {:system, "ASDF"}])
r = DeferredConfig.transform_cfg(key: {:system, "ASDF"})
assert r[:key] == nil
end

test "boolean environment variable `false` overrides default" do
System.put_env("BOOLEAN", "false")

cfg = [
bool_conf: {:system, "BOOLEAN", true, {String, :to_atom}}
]

actual = DeferredConfig.transform_cfg(cfg)

assert actual[:bool_conf] === false
end

test "readme sys/mfa example", %{transforms: transforms} do
readme_example = [
http: %{ # even inside nested data
# even inside nested data
http: %{
# the common 'system tuple' pattern is fully supported
port: {:system, "PORT", {String, :to_integer}}
},
# more general 'mfa tuple' pattern is also supported
key: {:apply, {MyMod, :get_my_key, ["arg"]}}
]
actual = readme_example
|> DeferredConfig.transform_cfg(transforms)

assert "your key is"<>_ = actual[:key]

actual =
readme_example
|> DeferredConfig.transform_cfg(transforms)

assert "your key is" <> _ = actual[:key]
assert actual[:http][:port] == 4000
end

defp delete_all_env(app) do
app
|> Application.get_all_env
|> Application.get_all_env()
|> Enum.each(fn {k, v} ->
Application.delete_env( app, k )
Application.delete_env(app, k)
end)
end

end