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
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
93 changes: 46 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,20 @@ 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 +158,27 @@ 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
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

59 changes: 33 additions & 26 deletions lib/replacing_walk.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@ defmodule ReplacingWalk do
"""

require Logger

@doc """
Recursive replacing walk that uses `recognize` and
`transform` functions to return a transformed version
of arbitrary data.
of arbitrary data.

iex> ReplacingWalk.walk [1, 2, 3], &(&1 == 2), &(&1 * &1)
[1,4,3]

iex> ReplacingWalk.walk( [1, [2, [3, 2]]],
...> &(&1 == 2),
...> &(&1 * &1)
...> &(&1 == 2),
...> &(&1 * &1)
...> )
[1,[4, [3, 4]]]

It works for Maps:

iex> ReplacingWalk.walk %{2 => 1, 1 => 2}, &(&1 == 2), &(&1 * &1)
%{4 => 1, 1 => 4}
Structs in general are considered as leaf nodes; we support

Structs in general are considered as leaf nodes; we support
structs that implement Enumerable, but **currently we expect
their `Enumerable` implementation to work like a Map.
If you feed this an Enumerable struct that doesn't iterate
like Map -- ie, doesn't iterate over `{k, v}` -- it will die.
If you feed this an Enumerable struct that doesn't iterate
like Map -- ie, doesn't iterate over `{k, v}` -- it will die.
(See an example in tests).

We may change that behavior in the future -- either removing
support for arbitrary Enumerables, or provision another protocol
that can be implemented to make a data type replacing-walkable.
Expand All @@ -42,17 +42,21 @@ defmodule ReplacingWalk do
`:deferred_config`, so it's probably got some holes;
tests that break it are welcome.
"""

# lists
def walk(_data = [], _recognize, _transform), do: []

def walk([item | ls], recognize, transform) do
item = item |> maybe_transform_leaf(recognize, transform)
[ walk(item, recognize, transform) |
walk(ls, recognize, transform) ]

[
walk(item, recognize, transform)
| walk(ls, recognize, transform)
]
end

# structs (enumerable and not; see notes about Enumerable)
def walk(m = %{ :__struct__ => _ }, recognize, transform) do
def walk(m = %{:__struct__ => _}, recognize, transform) do
if Enumerable.impl_for(m) do
m |> walk_map(recognize, transform)
else
Expand All @@ -64,46 +68,49 @@ defmodule ReplacingWalk do
def walk(m, recognize, transform) when is_map(m) do
m |> walk_map(recognize, transform)
end

def walk(%{}, _, _), do: %{}

# kv tuples (very common in config)
def walk(t = {k,v}, recognize, transform) do
def walk(t = {_k, _v}, recognize, transform) do
t = maybe_transform_leaf(t, recognize, transform)
if is_tuple t do

if is_tuple(t) do
{k, v} = t
{k |> walk(recognize, transform),
v |> walk(recognize, transform) }
else t end
{k |> walk(recognize, transform), v |> walk(recognize, transform)}
else
t
end
end

# any other data (other tuples; structs; str, atoms, nums..)
def walk(other, recognize, transform) do
recognize.(other) |> maybe_do(transform, other)
end

# -- impl details for map and maplike enum support
defp walk_map(m, recognize, transform) do

m = m |> maybe_transform_leaf(recognize, transform)

# due to above, may not be enumerable any more.
# also, could be untransformed enumerable, but with
# non-map-like iteration, which we *can't* detect without trying.
try do
Enum.reduce(m, %{}, fn {k, v}, acc ->
k = recognize.(k) |> maybe_do( transform, k )
k = recognize.(k) |> maybe_do(transform, k)
acc |> Map.put(k, walk(v, recognize, transform))
end)
catch _ ->
Logger.error("replacing walk: reduce failed for: #{inspect m}")
catch
_ ->
Logger.error("replacing walk: reduce failed for: #{inspect(m)}")
m
end
end

defp maybe_transform_leaf(o, recognize, transform) do
recognize.(o) |> maybe_do(transform, o)
end
defp maybe_do(_should_i = true, op, item), do: op.(item)
defp maybe_do(_shouldnt, _op, item), do: item

defp maybe_do(_should_i = true, op, item), do: op.(item)
defp maybe_do(_shouldnt, _op, item), do: item
end
Loading