diff --git a/README.md b/README.md index e70e184..1e82af4 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,9 @@ defmodule MySystem.Application do use Application def start(_type, _args) do - MySystem.Config.validate!() + children = [ + MySystem.Config + ] # ... end diff --git a/lib/provider.ex b/lib/provider.ex index 65510ac..425469a 100644 --- a/lib/provider.ex +++ b/lib/provider.ex @@ -21,8 +21,7 @@ defmodule Provider do This will generate the following functions in the module: - - `fetch_all` - retrieves values of all parameters - - `validate!` - validates that all parameters are correctly provided + - `load!` - validates that all parameters are correctly provided and stores them in the cache - `db_host`, `db_name`, `db_pool_size`, ... - getter of each declared parameter ## Describing params @@ -130,22 +129,6 @@ defmodule Provider do end end - @doc "Retrieves a single parameter." - @spec fetch_one(source, param_name, param_spec) :: {:ok, value} | {:error, [String.t()]} - def fetch_one(source, param_name, param_spec) do - with {:ok, map} <- fetch_all(source, %{param_name => param_spec}), - do: {:ok, Map.fetch!(map, param_name)} - end - - @doc "Retrieves a single param, raising if the value is not available." - @spec fetch_one!(source, param_name, param_spec) :: value - def fetch_one!(source, param, param_spec) do - case fetch_one(source, param, param_spec) do - {:ok, value} -> value - {:error, errors} -> raise Enum.join(errors, ", ") - end - end - # ------------------------------------------------------------------------ # Private # ------------------------------------------------------------------------ @@ -199,25 +182,37 @@ defmodule Provider do |> Keyword.fetch!(:params) |> Enum.map(fn {name, spec} -> {name, quote(do: %{unquote_splicing(spec)})} end) - @doc "Retrieves all parameters." - @spec fetch_all :: {:ok, %{unquote_splicing(typespecs)}} | {:error, [String.t()]} - def fetch_all do - Provider.fetch_all( - unquote(Keyword.fetch!(spec, :source)), + use GenServer - # quoted_params is itself a keyword list, so we need to convert it into a map - %{unquote_splicing(quoted_params)} - ) + @spec start_link(term()) :: GenServer.on_start() + def start_link(arg) do + GenServer.start_link(__MODULE__, arg, name: __MODULE__) end - @doc "Validates all parameters, raising if some values are missing or invalid." - @spec validate!() :: :ok - def validate! do - with {:error, errors} <- fetch_all() do - raise "Following OS env var errors were found:\n#{Enum.join(Enum.sort(errors), "\n")}" - end + @impl GenServer + def init(_arg) do + :ets.new(__MODULE__, [:named_table, {:read_concurrency, true}, :public]) + load!() + {:ok, nil} + end - :ok + @doc "Loads and validates all parameters, raising if some values are missing or invalid." + @spec load!() :: :ok + def load! do + case Provider.fetch_all( + unquote(Keyword.fetch!(spec, :source)), + %{ + unquote_splicing(quoted_params) + } + ) do + {:ok, values} -> + :ets.insert(__MODULE__, Map.to_list(values)) + + :ok + + {:error, errors} -> + raise "Following OS env var errors were found:\n#{Enum.join(Enum.sort(errors), "\n")}" + end end # Generate getter for each param. @@ -229,11 +224,8 @@ defmodule Provider do # bug in credo spec check # credo:disable-for-next-line Credo.Check.Readability.Specs def unquote(param_name)() do - Provider.fetch_one!( - unquote(Keyword.fetch!(spec, :source)), - unquote(param_name), - unquote(param_spec) - ) + [{unquote(param_name), value}] = :ets.lookup(__MODULE__, unquote(param_name)) + value end end ) diff --git a/test/provider_test.exs b/test/provider_test.exs index d5656be..d092bff 100644 --- a/test/provider_test.exs +++ b/test/provider_test.exs @@ -1,99 +1,11 @@ defmodule ProviderTest do use ExUnit.Case, async: true alias Provider + alias ProviderTest.ProcDictCache alias ProviderTest.TestModule - describe "fetch_one" do - test "returns correct value" do - param = param_spec() - System.put_env(param.os_env_name, "some value") - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, "some value"} - end - - test "returns default value if OS env is not set" do - param = param_spec(default: "default value") - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == - {:ok, "default value"} - end - - test "ignores default value and returns OS env value if it's available" do - param = param_spec(default: "default value") - System.put_env(param.os_env_name, "os env value") - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == - {:ok, "os env value"} - end - - test "converts to integer" do - param = param_spec(type: :integer, default: 123) - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, 123} - - System.put_env(param.os_env_name, "456") - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, 456} - end - - test "converts to float" do - param = param_spec(type: :float, default: 3.14) - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, 3.14} - - System.put_env(param.os_env_name, "2.72") - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, 2.72} - end - - test "converts to boolean" do - param = param_spec(type: :boolean, default: true) - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, true} - - System.put_env(param.os_env_name, "false") - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == {:ok, false} - end - - test "reports error on missing value" do - param = param_spec() - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == - {:error, [error(param, "is missing")]} - end - - test "empty string is treated as a missing value" do - param = param_spec() - System.put_env(param.os_env_name, "") - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == - {:error, [error(param, "is missing")]} - end - - for type <- ~w/integer float boolean/a do - test "reports error on #{type} conversion" do - param = param_spec(type: unquote(type), default: 123) - System.put_env(param.os_env_name, "invalid value") - - assert Provider.fetch_one(Provider.SystemEnv, param.name, param.opts) == - {:error, [error(param, "is invalid")]} - end - end - end - - describe "fetch_one!" do - test "returns correct value" do - param = param_spec() - System.put_env(param.os_env_name, "some value") - assert Provider.fetch_one!(Provider.SystemEnv, param.name, param.opts) == "some value" - end - - test "returns default value if OS env is not set" do - param = param_spec() - - assert_raise( - RuntimeError, - "#{param.os_env_name} is missing", - fn -> Provider.fetch_one!(Provider.SystemEnv, param.name, param.opts) end - ) - end + setup_all do + Application.put_env(:provider, :cache, ProcDictCache) end describe "fetch_all" do @@ -130,45 +42,20 @@ defmodule ProviderTest do Enum.each(1..7, &System.delete_env("OPT_#{&1}")) end - test "fetch_all/0 succeeds for correct data" do - System.put_env("OPT_1", "qux") - System.put_env("OPT_2", "42") - System.put_env("OPT_6", "false") - System.put_env("OPT_7", "3.14") - - assert TestModule.fetch_all() == - {:ok, - %{ - opt_1: "qux", - opt_2: 42, - opt_3: "foo", - opt_4: "bar", - opt_5: "baz", - opt_6: false, - opt_7: 3.14 - }} - end - - test "fetch_all/0 returns errors for invalid data" do - assert TestModule.fetch_all() == - { - :error, - ["OPT_1 is missing", "OPT_2 is missing", "OPT_6 is missing", "OPT_7 is missing"] - } - end - - test "validate!/0 succeeds for correct data" do + test "load!/0 succeeds for correct data" do System.put_env("OPT_1", "some data") System.put_env("OPT_2", "42") System.put_env("OPT_6", "false") System.put_env("OPT_7", "3.14") - assert TestModule.validate!() == :ok + start_config_server!() + + assert TestModule.load!() == :ok end - test "validate!/0 raises on error" do + test "load!/0 raises on error" do System.put_env("OPT_2", "foobar") - error = assert_raise RuntimeError, fn -> TestModule.validate!() end + error = assert_raise RuntimeError, fn -> TestModule.load!() end assert error.message =~ "OPT_1 is missing" assert error.message =~ "OPT_2 is invalid" assert error.message =~ "OPT_6 is missing" @@ -181,6 +68,8 @@ defmodule ProviderTest do System.put_env("OPT_6", "false") System.put_env("OPT_7", "3.14") + start_config_server!() + assert TestModule.opt_1() == "some data" assert TestModule.opt_2() == 42 assert TestModule.opt_3() == "foo" @@ -190,10 +79,6 @@ defmodule ProviderTest do assert TestModule.opt_7() == 3.14 end - test "access function raises for on error" do - assert_raise RuntimeError, "OPT_1 is missing", fn -> TestModule.opt_1() end - end - test "template/0 generates config template" do assert TestModule.template() == """ @@ -230,6 +115,10 @@ defmodule ProviderTest do defp error(param, message), do: "#{param.os_env_name} #{message}" + defp start_config_server! do + start_supervised!(TestModule, restart: :temporary) + end + defmodule TestModule do baz = "baz"