Skip to content
Merged
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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ defmodule MySystem.Application do
use Application

def start(_type, _args) do
MySystem.Config.validate!()
children = [
MySystem.Config
]

# ...
end
Expand Down
68 changes: 30 additions & 38 deletions lib/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
# ------------------------------------------------------------------------
Expand Down Expand Up @@ -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.
Expand All @@ -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
)
Expand Down
141 changes: 15 additions & 126 deletions test/provider_test.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -190,10 +79,6 @@ defmodule ProviderTest do
assert TestModule.opt_7() == 3.14
end

test "access function raises for on error" do
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is no longer applicable since we are expecting someone to call MyConfig.start_link() before attempting to use the values. And if we did that before the test like we did in other places, we would get an error at that point.

assert_raise RuntimeError, "OPT_1 is missing", fn -> TestModule.opt_1() end
end

test "template/0 generates config template" do
assert TestModule.template() ==
"""
Expand Down Expand Up @@ -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"

Expand Down
Loading