diff --git a/lib/deferred_config.ex b/lib/deferred_config.ex index b0ab201..247c929 100644 --- a/lib/deferred_config.ex +++ b/lib/deferred_config.ex @@ -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. @@ -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: @@ -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 @@ -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. @@ -86,25 +84,26 @@ 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 @@ -112,7 +111,7 @@ defmodule DeferredConfig do @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 @@ -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) @@ -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"}` @@ -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 - diff --git a/mix.lock b/mix.lock index 0e99ff4..3a0182d 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, +} diff --git a/test/deferred_config_test.exs b/test/deferred_config_test.exs index a7eb89f..bd14b52 100644 --- a/test/deferred_config_test.exs +++ b/test/deferred_config_test.exs @@ -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 @@ -34,12 +35,15 @@ 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" @@ -47,7 +51,7 @@ defmodule DeferredConfigTest do 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" @@ -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