├── test ├── test_helper.exs ├── replacing_walk_test.exs └── deferred_config_test.exs ├── mix.lock ├── .gitignore ├── LICENSE ├── config └── config.exs ├── mix.exs ├── lib ├── replacing_walk.ex └── deferred_config.ex └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, 2 | "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, 3 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}} 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Luc Fueston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :deferred_config, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:deferred_config, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule DeferredConfig.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :deferred_config, 6 | version: "0.1.1", 7 | elixir: "~> 1.4-rc", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | consolidate_protocols: Mix.env != :test, # proto impl tests 11 | deps: deps(), 12 | 13 | name: "DeferredConfig", 14 | package: package(), 15 | description: description(), 16 | source_url: "https://github.com/mrluc/deferred_config", 17 | homepage_url: "https://github.com/mrluc/deferred_config", 18 | docs: [main: "readme", 19 | extras: ["README.md"]] 20 | ] 21 | end 22 | 23 | def description do 24 | "Seamless runtime config with one line of code. "<> 25 | "No special accessors or mappings. Full support for " <> 26 | "'{:system...} tuple' and '{m,f,a}' runtime config patterns." 27 | end 28 | 29 | def package do 30 | [ 31 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 32 | maintainers: ["Luc Fueston"], 33 | licenses: ["MIT"], 34 | links: %{"GitHub" => "https://github.com/mrluc/deferred_config", 35 | "Docs" => "https://hexdocs.pm/deferred_config/readme.html"} 36 | ] 37 | end 38 | 39 | def application do 40 | [extra_applications: [:logger]] 41 | end 42 | 43 | defp deps do 44 | [{:ex_doc, "~> 0.14", only: :dev}, 45 | {:credo, "~> 0.5", only: [:dev, :test]}] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/replacing_walk_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ham do defstruct( a: 1 ) end 2 | defmodule Spam do defstruct( a: 1, b: 2 ) end 3 | defimpl Enumerable, for: Spam do 4 | def count(_), do: {:error, __MODULE__} # default reduce-based 5 | def member?(_,_), do: {:error, __MODULE__} # default reduce-based 6 | def reduce(%{a: a, b: b}, {_, acc}, fun) do 7 | {:cont, acc} = fun.({:a, a}, acc) 8 | {:cont, acc} = fun.({:b, b}, acc) 9 | {:done, acc} 10 | end 11 | end 12 | 13 | defmodule EnvTest do 14 | use ExUnit.Case 15 | doctest DeferredConfig 16 | alias ReplacingWalk, as: RW 17 | 18 | test "basic " do 19 | data = [:a, :b, :c] 20 | expected = [:balls, :b, :c] 21 | actual = data 22 | |> RW.walk( &recognize_atom_a/1, &transform_to_balls/1 ) 23 | assert expected == actual 24 | end 25 | 26 | test "maps" do 27 | data = %{ :a => 1, :b => 2, :a => :a } 28 | expected = %{ :balls => 1, :b => 2, :balls => :balls} 29 | actual = data 30 | |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) 31 | assert expected == actual 32 | end 33 | 34 | test "map - kitchen sink" do 35 | assert example_data().kitchen_sink_map_a_to_balls == 36 | example_data().kitchen_sink_map_a 37 | |> RW.walk( &recognize_atom_a/1, &transform_to_balls/1) 38 | end 39 | 40 | test "structs won't change if they're not enumerable" do 41 | data = %Ham{ a: 2} 42 | expected = %Ham{ a: 2} 43 | actual = data 44 | |> RW.walk(&recognize_atom_a/1, &transform_to_balls/1) 45 | assert expected == actual 46 | end 47 | 48 | test "enumerable types are ok tho" do 49 | recognize = fn i -> i == 3 end 50 | data = %Spam{ a: 2, b: 3} 51 | assert Enumerable.impl_for(data) == Enumerable.Spam 52 | expected = %{ a: 2, b: :balls} 53 | actual = data 54 | |> RW.walk(recognize, &transform_to_balls/1) 55 | assert expected == actual 56 | end 57 | 58 | def recognize_atom_a(:a), do: true 59 | def recognize_atom_a(_), do: false 60 | def transform_to_balls(_), do: :balls 61 | 62 | def example_data do 63 | %{kitchen_sink_map_a: 64 | %{ 65 | :a => 1, # replaceable key 66 | :b => 2, 67 | :c => [ 68 | [ :a, {:a, 3} ], # list 69 | [ a: 2, a: 3 ] # kvlist 70 | ], 71 | :d => :a # replaceable value 72 | }, 73 | kitchen_sink_map_a_to_balls: 74 | %{ 75 | :balls => 1, # replaceable key 76 | :b => 2, 77 | :c => [ 78 | [ :balls, {:balls, 3} ], # tuples NOT replaced yet 79 | [ balls: 2, balls: 3 ] # kvlist NOT replaced yet 80 | ], 81 | :d => :balls # replaceable value 82 | } 83 | } 84 | end 85 | end 86 | 87 | 88 | -------------------------------------------------------------------------------- /test/deferred_config_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DeferredConfigTest do 2 | use ExUnit.Case 3 | doctest DeferredConfig 4 | 5 | @app :lazy_cfg_test_appname 6 | 7 | defmodule MyMod do 8 | def get_my_key(""<>bin), do: "your key is 1234. write it down." 9 | end 10 | 11 | setup do 12 | delete_all_env(@app) 13 | # give each test a fake env that looks like this 14 | env = %{"PORT" => "4000"} 15 | system_transform = fn 16 | {:system, k} -> Map.get(env, k) 17 | {:system, k, {m, f}} -> apply m, f, [Map.get(env, k)] 18 | {:system, k, d} -> Map.get(env, k, d) 19 | {:system, k, d, {m, f}} -> apply( m, f, [Map.get(env, k)]) || d 20 | end 21 | # our mock stack -- only changes env var retrieval 22 | transforms = [ 23 | {&DeferredConfig.recognize_system_tuple/1, system_transform}, 24 | {&DeferredConfig.recognize_mfa_tuple/1, 25 | &DeferredConfig.transform_mfa_tuple/1} 26 | ] 27 | [transforms: transforms, 28 | system_transform: system_transform] 29 | end 30 | 31 | test "system tuples support", %{system_transform: transform} do 32 | cfg = [ 33 | port1: {:system, "PORT"}, 34 | port2: {:system, "PORT", "1111"}, 35 | port3: {:system, "FAIL", "1111"}, 36 | port4: {:system, "PORT", {String, :to_integer}}, 37 | port5: [{:system, "PORT", 3000, {String, :to_integer}}], 38 | ] 39 | actual = cfg 40 | |> DeferredConfig.transform_cfg([ 41 | {&DeferredConfig.recognize_system_tuple/1, transform} 42 | ]) 43 | assert actual[:port1] == "4000" 44 | assert actual[:port2] == "4000" 45 | assert actual[:port3] == "1111" 46 | assert actual[:port4] == 4000 47 | assert actual[:port5] == [4000] 48 | actual |> DeferredConfig.apply_transformed_cfg!(@app) 49 | 50 | actual = Application.get_all_env @app 51 | assert actual[:port1] == "4000" 52 | assert actual[:port2] == "4000" 53 | assert actual[:port3] == "1111" 54 | assert actual[:port4] == 4000 55 | assert actual[:port5] == [4000] 56 | end 57 | 58 | test "non-existent tuple values are handled" do 59 | r = DeferredConfig.transform_cfg([key: {:system, "ASDF"}]) 60 | assert r[:key] == nil 61 | end 62 | test "readme sys/mfa example", %{transforms: transforms} do 63 | readme_example = [ 64 | http: %{ # even inside nested data 65 | # the common 'system tuple' pattern is fully supported 66 | port: {:system, "PORT", {String, :to_integer}} 67 | }, 68 | # more general 'mfa tuple' pattern is also supported 69 | key: {:apply, {MyMod, :get_my_key, ["arg"]}} 70 | ] 71 | actual = readme_example 72 | |> DeferredConfig.transform_cfg(transforms) 73 | 74 | assert "your key is"<>_ = actual[:key] 75 | assert actual[:http][:port] == 4000 76 | end 77 | 78 | defp delete_all_env(app) do 79 | app 80 | |> Application.get_all_env 81 | |> Enum.each(fn {k, v} -> 82 | Application.delete_env( app, k ) 83 | end) 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /lib/replacing_walk.ex: -------------------------------------------------------------------------------- 1 | defmodule ReplacingWalk do 2 | @moduledoc """ 3 | A hastily constructed replacing walk for use 4 | with `DeferredConfig`; not 5 | very performant, but for transforming data 6 | in options and config, can be convenient. 7 | """ 8 | 9 | require Logger 10 | 11 | @doc """ 12 | Recursive replacing walk that uses `recognize` and 13 | `transform` functions to return a transformed version 14 | of arbitrary data. 15 | 16 | iex> ReplacingWalk.walk [1, 2, 3], &(&1 == 2), &(&1 * &1) 17 | [1,4,3] 18 | 19 | iex> ReplacingWalk.walk( [1, [2, [3, 2]]], 20 | ...> &(&1 == 2), 21 | ...> &(&1 * &1) 22 | ...> ) 23 | [1,[4, [3, 4]]] 24 | 25 | It works for Maps: 26 | 27 | iex> ReplacingWalk.walk %{2 => 1, 1 => 2}, &(&1 == 2), &(&1 * &1) 28 | %{4 => 1, 1 => 4} 29 | 30 | Structs in general are considered as leaf nodes; we support 31 | structs that implement Enumerable, but **currently we expect 32 | their `Enumerable` implementation to work like a Map. 33 | If you feed this an Enumerable struct that doesn't iterate 34 | like Map -- ie, doesn't iterate over `{k, v}` -- it will die. 35 | (See an example in tests). 36 | 37 | We may change that behavior in the future -- either removing 38 | support for arbitrary Enumerables, or provision another protocol 39 | that can be implemented to make a data type replacing-walkable. 40 | 41 | Created quickly for 42 | `:deferred_config`, so it's probably got some holes; 43 | tests that break it are welcome. 44 | """ 45 | 46 | # lists 47 | def walk(_data = [], _recognize, _transform), do: [] 48 | def walk([item | ls], recognize, transform) do 49 | item = item |> maybe_transform_leaf(recognize, transform) 50 | [ walk(item, recognize, transform) | 51 | walk(ls, recognize, transform) ] 52 | end 53 | 54 | # structs (enumerable and not; see notes about Enumerable) 55 | def walk(m = %{ :__struct__ => _ }, recognize, transform) do 56 | if Enumerable.impl_for(m) do 57 | m |> walk_map(recognize, transform) 58 | else 59 | m |> maybe_transform_leaf(recognize, transform) 60 | end 61 | end 62 | 63 | # maps 64 | def walk(m, recognize, transform) when is_map(m) do 65 | m |> walk_map(recognize, transform) 66 | end 67 | def walk(%{}, _, _), do: %{} 68 | 69 | # kv tuples (very common in config) 70 | def walk(t = {k,v}, recognize, transform) do 71 | t = maybe_transform_leaf(t, recognize, transform) 72 | if is_tuple t do 73 | {k, v} = t 74 | {k |> walk(recognize, transform), 75 | v |> walk(recognize, transform) } 76 | else t end 77 | end 78 | 79 | # any other data (other tuples; structs; str, atoms, nums..) 80 | def walk(other, recognize, transform) do 81 | recognize.(other) |> maybe_do(transform, other) 82 | end 83 | 84 | # -- impl details for map and maplike enum support 85 | defp walk_map(m, recognize, transform) do 86 | 87 | m = m |> maybe_transform_leaf(recognize, transform) 88 | 89 | # due to above, may not be enumerable any more. 90 | # also, could be untransformed enumerable, but with 91 | # non-map-like iteration, which we *can't* detect without trying. 92 | try do 93 | Enum.reduce(m, %{}, fn {k, v}, acc -> 94 | k = recognize.(k) |> maybe_do( transform, k ) 95 | acc |> Map.put(k, walk(v, recognize, transform)) 96 | end) 97 | catch _ -> 98 | Logger.error("replacing walk: reduce failed for: #{inspect m}") 99 | m 100 | end 101 | end 102 | 103 | defp maybe_transform_leaf(o, recognize, transform) do 104 | recognize.(o) |> maybe_do(transform, o) 105 | end 106 | defp maybe_do(_should_i = true, op, item), do: op.(item) 107 | defp maybe_do(_shouldnt, _op, item), do: item 108 | 109 | end 110 | -------------------------------------------------------------------------------- /lib/deferred_config.ex: -------------------------------------------------------------------------------- 1 | defmodule DeferredConfig do 2 | @moduledoc """ 3 | Seamlessly add runtime config to your library, with the 4 | "system tuples" or the `{m,f,a}` patterns. 5 | 6 | # Seamlessly? 7 | 8 | In your application startup, add the following line: 9 | 10 | defmodule Mine.Application do 11 | def start(_type, _args) do 12 | DeferredConfig.populate(:mine) # <-- this one 13 | ... 14 | end 15 | end 16 | 17 | Where `:mine` is the name of your OTP app. 18 | 19 | Now you and users of your app or lib can configure 20 | as follows, and it'll work -- regardless of if they're 21 | running it from iex, or a release with env vars set: 22 | 23 | config :mine, 24 | 25 | # string from env var, or `nil` if missing. 26 | port1: {:system, "PORT"}, 27 | 28 | # string from env var |> integer; `nil` if missing. 29 | port2: {:system, "PORT", {String, :to_integer}}, 30 | 31 | # string from env var, or "4000" as default. 32 | port3: {:system, "PORT", "4000"}, 33 | 34 | # converts env var to integer, or 4000 as default. 35 | port4: {:system, "PORT", 4000, {String, :to_integer}} 36 | 37 | **Accessing config does not change.** 38 | 39 | Since you can use arbitrary transformation functions, 40 | you can do advanced transformations if you need to: 41 | 42 | # lib/mine/ip.ex 43 | defmodule Mine.Ip do 44 | @doc ":inet_res uses `{0,0,0,0}` for ipv4 addrs" 45 | def str2ip(str) do 46 | case :inet_parse:address(str) do 47 | {:ok, ip = {_, _, _, _}} -> ip 48 | {:error, _} -> nil 49 | end 50 | end 51 | end 52 | 53 | # config.exs 54 | config :my_app, 55 | port: {:system, "MY_IP", {127,0,0,1}, {Mine.Ip, :str2ip} 56 | 57 | See `README.md` for explanation of rationale. 58 | **TL;DR:** `REPLACE_OS_VARS` is string-only and release-only, 59 | and `{:system, ...}` support among libraries is spotty 60 | and easy to get wrong in ways that bite your users 61 | come release time. This library tries to make it easier 62 | to do the right thing with 1 LOC. Other libraries add special 63 | config files and/or special config accessors, which 64 | is more complex than necessary. 65 | """ 66 | require Logger 67 | import ReplacingWalk, only: [walk: 3] 68 | 69 | @default_rts [ 70 | {&DeferredConfig.recognize_system_tuple/1, 71 | &DeferredConfig.get_system_tuple/1}, 72 | {&DeferredConfig.recognize_mfa_tuple/1, 73 | &DeferredConfig.transform_mfa_tuple/1} 74 | ] 75 | 76 | @doc """ 77 | Populate deferred values in an app's config. 78 | Best run during `Application.start/2`. 79 | 80 | **By default** attempts to populate the common 81 | `{:system, "VAR"}` tuple form for getting values from 82 | `System.get_env/1`, and the more 83 | general `{:apply, {Mod, fun, [args]}}` form as well. 84 | 85 | System tuples support optional 86 | defaults and conversion functions, see 87 | `Peerage.DeferredConfig.get_system_tuple/1`. 88 | 89 | Can be extended by passing in a different 90 | enumerable of `{&recognizer/1, &transformer/1}` 91 | functions. 92 | """ 93 | def populate(app, transforms \\ @default_rts) do 94 | :ok = app 95 | |> Application.get_all_env 96 | |> transform_cfg(transforms) 97 | |> apply_transformed_cfg!(app) 98 | end 99 | 100 | @doc """ 101 | Given a config kvlist, and an enumerable of 102 | `{&recognize/1, &transform/1}` functions, 103 | returns a kvlist with the values transformed 104 | via replacing walk. 105 | """ 106 | def transform_cfg(cfg, rts \\ @default_rts) when is_list(rts) do 107 | Enum.map(cfg, fn {k,v} -> 108 | {k, apply_rts(v, rts)} 109 | end) 110 | end 111 | 112 | @doc "`Application.put_env/3` for config kvlist" 113 | def apply_transformed_cfg!(kvlist, app) do 114 | kvlist 115 | |> Enum.each(fn {k,v} -> 116 | Application.put_env(app, k, v) 117 | end) 118 | end 119 | 120 | @doc """ 121 | Default recognize/transform pairs used in 122 | populating deferred config. Currently 123 | r/t pairs for :system tuples and :apply mfa tuples. 124 | """ 125 | def default_transforms(), do: @default_rts 126 | 127 | # apply sequence of replacing walks to a value 128 | defp apply_rts(val, []), do: val 129 | defp apply_rts(val, rts) when is_list(rts) do 130 | Enum.reduce(rts, val, fn {r, t}, acc_v -> 131 | walk(acc_v, r, t) 132 | end) 133 | end 134 | 135 | @doc """ 136 | Recognize mfa tuple, like `{:apply, {File, :read!, ["name"]}}`. 137 | Returns `true` on recognition, `false` otherwise. 138 | """ 139 | def recognize_mfa_tuple({:apply, {m,f,a}}) 140 | when is_atom(m) and is_atom(f) and is_list(a), 141 | do: true 142 | def recognize_mfa_tuple({:apply, t}) do 143 | Logger.error "badcfg - :apply needs {:m, :f, lst}. "<> 144 | "given: #{ inspect t }" 145 | false 146 | end 147 | def recognize_mfa_tuple(_), do: false 148 | 149 | @doc "Return evaluated `{:apply, {mod, fun, args}}` tuple." 150 | def transform_mfa_tuple({:apply, {m,f,a}}), do: apply(m,f,a) 151 | 152 | 153 | @doc """ 154 | Recognizer for system tuples of forms: 155 | - `{:system, "VAR"}` 156 | - `{:system, "VAR", default_value}` 157 | - `{:system, "VAR", {String, :to_integer}}` 158 | - `{:system, "VAR", default_value, {String, :to_integer}}` 159 | Returns `true` when it matches one, `false` otherwise. 160 | """ 161 | def recognize_system_tuple({:system, ""<>_k}), do: true 162 | def recognize_system_tuple({:system, ""<>_k, _default}), do: true 163 | def recognize_system_tuple({:system, ""<>_k, _d, _mf}), do: true 164 | def recognize_system_tuple(_), do: false 165 | 166 | @doc """ 167 | Return transformed copy of recognized system tuples: 168 | gets from env, optionally converts it, with 169 | optional default if env returned nothing. 170 | """ 171 | def get_system_tuple({:system, k}), do: System.get_env(k) 172 | def get_system_tuple({:system, k, {m, f}}) do 173 | apply m, f, [ System.get_env(k) ] 174 | end 175 | def get_system_tuple({:system, k, d}), do: System.get_env(k) || d 176 | def get_system_tuple({:system, k, d, {m, f}}) do 177 | (val = System.get_env k) && apply(m, f, [val]) || d 178 | end 179 | def get_system_tuple(t), do: throw "Could not fetch: #{inspect t}" 180 | 181 | 182 | 183 | 184 | end 185 | 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Deferred Config 2 | 3 | 4 | **Stable but DEPRECATED, just because in the past few years Elixir itself has gained ample support for runtime config, built right in! `release.exs` and other means -- so if you're coming here and wondering if this meets a need for you, you *probably* don't need it.** 5 | 6 | Seamless runtime config with one line of code. In 7 | your application's `start/2` method, call: 8 | 9 | ```elixir 10 | DeferredConfig.populate(:otp_app_name) 11 | ``` 12 | 13 | And now you and users of your application or library 14 | will be able to write config that is deferred to 15 | runtime, like the following: 16 | 17 | ```elixir 18 | config :otp_app_name, 19 | http: %{ # nested config is ok 20 | # common 'system tuple' pattern is fully supported 21 | port: {:system, "PORT", {String, :to_integer}} 22 | }, 23 | # more general 'mfa tuple' pattern is also supported 24 | secret_key: {:apply, {MyKey, :fetch, ["arg"]}} 25 | ``` 26 | 27 | That's it. 28 | 29 | - No 'mappings,' no special access methods -- just 30 | keep using `Application.get_env/2`. 31 | - Works for arbitrarily nested config. 32 | - Works just as well when run with mix as it does 33 | in releases built with `:distillery`, `:exrm`, 34 | or `:relx`. 35 | - Lets library authors support the 36 | common "system tuples" pattern *effortlessly.* 37 | 38 | **Why this library?** 39 | 40 | See '[Rationale](#rationale)' for more detail. But 41 | **TLDR:** `REPLACE_OS_VARS` is string-only and 42 | release-only, and `{:system, ...}` support among 43 | libraries is inconsistent and easy to get wrong in 44 | ways that bite your users come release time -- in 45 | other words, until now it's been a burden on 46 | library authors. This library tries to make it 47 | 1 LOC to do the right thing. 48 | 49 | There are other libraries to manage runtime config 50 | (see list at end of readme) but using them is harder 51 | as they add things -- like special config accessor functions, 52 | and/or their own config files, or mappings, or DSLs. We don't 53 | need to, because we rely on a `ReplacingWalk` 54 | of an app's config during `Application.start/2`, and 55 | the only DSL are configuration patterns sourced from 56 | the community, like system and mfa tuples. 57 | 58 | 59 | ## Usage 60 | 61 | In mix.exs, 62 | 63 | ```elixir 64 | defp deps, do: [{:deferred_config, "~> 0.1.0"}] 65 | ``` 66 | 67 | Then, in your application startup, add the following line: 68 | 69 | ```elixir 70 | defmodule Mine.Application do 71 | ... 72 | def start(_type, _args) do 73 | 74 | DeferredConfig.populate(:mine) # <--- 75 | 76 | ... 77 | end 78 | end 79 | ``` 80 | 81 | Where the app name is `:mine`. 82 | 83 | Now, you and users of your app can configure 84 | as follows, and it'll work -- regardless of if they're 85 | running it from iex, or a release with env vars set: 86 | 87 | ```elixir 88 | config :mine, 89 | 90 | # string from env var, or `nil` if missing. 91 | port1: {:system, "PORT"}, 92 | 93 | # string from env var |> integer; `nil` if missing. 94 | port2: {:system, "PORT", {String, :to_integer}}, 95 | 96 | # string from env var, or "4000" as default. 97 | port3: {:system, "PORT", "4000"}, 98 | 99 | # converts env var to integer, or 4000 as default. 100 | port4: {:system, "PORT", 4000, {String, :to_integer}} 101 | ``` 102 | 103 | ## Features 104 | 105 | **Accessing config does not change.** If you used 106 | `Application.get_env(:mine, :port1)` before, that will 107 | keep working. 108 | 109 | Since you can use arbitrary transformation functions, 110 | **you can do advanced transformations** if you need to: 111 | 112 | ```elixir 113 | # --- lib/mine/ip.ex 114 | defmodule Mine.Ip do 115 | @doc ":inet uses `{0,0,0,0}` for ipv4 addrs" 116 | def str2ip(str) do 117 | case :inet_parse:address(str) do 118 | {:ok, ip = {_, _, _, _}} -> ip 119 | {:error, _} -> nil 120 | end 121 | end 122 | end 123 | 124 | # --- config.exs 125 | config :my_app, 126 | port: {:system, "MY_IP", {127,0,0,1}, {Mine.Ip, :str2ip} 127 | ``` 128 | 129 | If you need even more control -- say, the 130 | source of your config isn't the system env, but a file 131 | in a directory, which is more secure in some use 132 | cases -- you can use the deferred MFA (module, function, 133 | arguments) form: 134 | 135 | ```elixir 136 | config :mine, 137 | api_key: {:apply, {File, :read!, ["k.txt"]}} 138 | ``` 139 | 140 | **Nested and arbitrary config** should work. 141 | 142 | **Can be extended** to recognize and transform 143 | other kinds of config as well (`DeferredConfig.populate/2`), 144 | ie if there's a pattern like 'system tuples' that you 145 | wanted to support, and `{:apply, mfa}` was bad UX. 146 | 147 | If you have another use case that this doesn't cover, 148 | please file an issue or reach out to github.com/mrluc 149 | 150 | ### Limitations 151 | 152 | Note that this only applies to **one OTP app's config.** 153 | We can't (and shouldn't try to) monkey-patch every app's 154 | config; they all start up at different times. 155 | 156 | This limitation applies to all approaches to runtime 157 | config except `REPLACE_OS_VARS`. 158 | 159 | 160 | ## Rationale 161 | 162 | Mix configs don't always work like users would 163 | like when they build releases, whether with relx, exrm, 164 | distillery, or something else. 165 | 166 | There are 3 approaches we'll look at to identify pain points: 167 | 168 | 1. `REPLACE_OS_VARS` for releases 169 | 2. `{:system, ...}` tuples for deferred config 170 | 3. Other runtime config libraries 171 | 172 | 173 | ### 1) REPLACE_OS_VARS is for releases only 174 | 175 | The best-supported method 176 | of injecting run-time configuration -- running the release 177 | with `REPLACE_OS_VARS` or `RELX_REPLACE_OS_VARS`, supported 178 | by `distillery`, `relx` and `exrm` -- will result in 179 | config like the following: 180 | 181 | config :my_app, field: "${SOME_VAR}" 182 | 183 | That works in **all your config, for all apps you configure**, 184 | even if the app doesn't do anything particular to support it. 185 | 186 | Drawbacks of `REPLACE_OS_VARS` 187 | 188 | - It only works when running a release. 189 | Otherwise, your `DB_URL` will literally be `"${DB_URL}"`. 190 | - It only gives you string values. Some libs will require 191 | that eg `PORT` be a number. 192 | 193 | Neither is a show-stopper *by any means*, but it's 194 | a small complication ... shared users of thousands of 195 | libraries. 196 | 197 | ### 2) `{:system, ...}` tuples have inconsistent support 198 | 199 | Apps that want to allow 200 | run-time configuration from Mix configs (which you could 201 | argue is 'all of them') should be configurable 202 | with lazy values, which can be filled **on startup of 203 | that application, before they are used**. 204 | 205 | What should those lazy values look like? Many libraries have 206 | settled on so-called 'system tuples', like: 207 | 208 | config :someapp, 209 | field: {:system, "ENV_VAR_NAME", "default value"} 210 | 211 | **The downside**: that approach requires every 212 | library author to recognize and support that kind 213 | of tuple. 214 | 215 | Some big libraries do! However, it can be a pain to add 216 | support for that kind of config consistently, converting 217 | data types appropriately, for all configurable options in 218 | your app. (A small pain, spread over many libraries). 219 | This library automates that pattern. 220 | 221 | 222 | ### 3) Other runtime config libs use special config files and/or access methods 223 | 224 | There are many other libs for config, most of which also 225 | deal with runtime config: 226 | 227 | - [:confex](https://hexdocs.pm/confex) 228 | - [:flasked](https://hexdocs.pm/flasked) 229 | - [:env_config](https://hexdocs.pm/env_config) 230 | - [:config_ext](https://hexdocs.pm/config_ext) 231 | - [libex_config](https://hex.pm/packages/libex_config) 232 | - [:configparser](https://hexdocs.pm/configparser_ex) 233 | - [:config](https://hexdocs.pm/config) 234 | - [:spellbook](https://hex.pm/packages/spellbook) 235 | 236 | They solve a wide variety of config-related problems. 237 | 238 | However, these **all** introduce their own methods for 239 | accessing Application config, and other complexity 240 | as well (mappings, config files, etc). 241 | 242 | We avoid that, by doing a replacing walk on the 243 | app's config at startup. 244 | 245 | 246 | ## 'When should I REPLACE_OS_VARS?' 247 | 248 | Always, but not always because of app config! 249 | 250 | For injecting config, it has the limitations 251 | mentioned above in 'Rationale.' 252 | 253 | - For config: **if** you need to configure many libraries that don't 254 | support deferred config, **and** what you want to configure 255 | can be a string (`DB_URL`, for instance) ... in 256 | that case, maybe a release-only config is a good option. 257 | 258 | But you should probably use `REPLACE_OS_VARS` 259 | (or `RELX_REPLACE_OS_VARS`), because it also 260 | allows **interpolation in `vm.args`**. 261 | 262 | - That lets you drive node short/longnames in releases with env vars. 263 | Which can be important when eg you don't know the node 264 | IP for a release at compile-time. 265 | - It's nice that the same approach for vm.args templating 266 | works across release builders. 267 | 268 | --------------------------------------------------------------------------------