diff --git a/docs/cheatsheets/cheatsheet.cheatmd b/docs/cheatsheets/cheatsheet.cheatmd index 239d4ff..1b5f28a 100644 --- a/docs/cheatsheets/cheatsheet.cheatmd +++ b/docs/cheatsheets/cheatsheet.cheatmd @@ -131,6 +131,18 @@ config :my_app, MyApp.PGRepo, password: env!("PG_PASSWORD", :string), port: env!("PG_PORT", :integer), hostname: env!("PG_HOSTNAME", :string) + +# `get_dotenv/2,3` (alias of `env!/3`, but with fallback to nil; default type is string) +config :my_app, + username: get_dotenv("PG_USERNAME"), # string type, fallback to nil + database: get_dotenv("PG_DATABASE", "my_app_db"), # string type, fallback to "my_app_db" + port: get_dotenv("PG_PORT", :integer), # fallback to nil + pool_size: get_dotenv("PG_POOL_SIZE", :integer, 10) # fallback to 10 + +# `fetch_dotenv!/2` (alias of `env!/2` - raises if value not found; default type is string) +config :my_app, + secret_key: fetch_dotenv!("SECRET_KEY"), + secret_number: fetch_dotenv!("SECRET_NUMBER", :integer!) ``` ## Transformations diff --git a/lib/dotenvy.ex b/lib/dotenvy.ex index d95c3ea..fffb224 100644 --- a/lib/dotenvy.ex +++ b/lib/dotenvy.ex @@ -162,6 +162,67 @@ defmodule Dotenvy do reraise error, __STACKTRACE__ end + @doc """ + Reads an env variable from the sourced dotenv store, converting its value to the given `type`, + or returns a `default` value. + + This is a convenience wrapper around `env!/3` that defaults to `nil` when no `default` is + provided, matching the convention of `System.get_env/2`. + + **The `default` value is returned as-is, without conversion**, consistent with `env!/3`. See + `env!/3` for full documentation. + + ## Examples + + Fallback values for different scenarios: + + iex> get_dotenv("NOT_SET") + nil + + iex> get_dotenv("NOT_SET", "default") + "default" + + iex> get_dotenv("NOT_SET", :integer!) + nil + + iex> get_dotenv("NOT_SET", :integer!, 5) + 5 + """ + @spec get_dotenv(variable :: binary()) :: any() + def get_dotenv(variable), do: env!(variable, :string, nil) + + @spec get_dotenv(variable :: binary(), default :: binary()) :: any() | no_return() + def get_dotenv(variable, default) when is_binary(default), do: env!(variable, :string, default) + + @spec get_dotenv( + variable :: binary(), + type :: Dotenvy.Transformer.conversion_type(), + default :: any() + ) :: any() | no_return() + def get_dotenv(variable, type, default \\ nil), do: env!(variable, type, default) + + @doc """ + Reads an env variable from the sourced dotenv store and converts its value to the given `type`. + Raises if the variable is not set. + + This is a convenience alias for `env!/2`, providing a name that matches the convention of + `System.fetch_env!/1`. See `env!/2` for full documentation. + + ## Examples + + iex> fetch_dotenv!("PORT", :integer!) + 5432 + + iex> fetch_dotenv!("NOT_SET") + ** (RuntimeError) Environment variable NOT_SET not set + """ + @spec fetch_dotenv!(variable :: binary()) :: any() | no_return() + def fetch_dotenv!(variable), do: env!(variable) + + @spec fetch_dotenv!(variable :: binary(), type :: Dotenvy.Transformer.conversion_type()) :: + any() | no_return() + def fetch_dotenv!(variable, type), do: env!(variable, type) + @doc """ Like its Bash namesake command, `source/2` accumulates values from the given input(s). The accumulated values are stored via a side effect function to make them available diff --git a/test/dotenvy_test.exs b/test/dotenvy_test.exs index 3557772..ff91937 100644 --- a/test/dotenvy_test.exs +++ b/test/dotenvy_test.exs @@ -105,6 +105,119 @@ defmodule DotenvyTest do end end + describe "get_dotenv/3" do + test "default type is string", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert get_dotenv("TEST_VALUE") |> is_binary() + end + + test "returns nil default when variable not set" do + assert nil == get_dotenv("DOES_NOT_EXIST") + assert nil == get_dotenv("DOES_NOT_EXIST", :string) + end + + test "returns default when variable not set" do + assert "some-default" = get_dotenv("DOES_NOT_EXIST", "some-default") + assert "some-default" = get_dotenv("DOES_NOT_EXIST", :string, "some-default") + end + + test "returns value when env set and sourced", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + assert "#{test}" == get_dotenv("TEST_VALUE", :string, nil) + end + + test "returns nil default when env set but not sourced", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + + assert nil == get_dotenv("TEST_VALUE", :string) + + source([System.get_env()]) + assert "#{test}" == get_dotenv("TEST_VALUE", :string, nil) + end + + test "built-in conversion errors convert to RuntimeError", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise RuntimeError, fn -> + get_dotenv("TEST_VALUE", :integer, 123) + end + end + + test "raising Dotenvy.Error with custom message converts to RuntimeError", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise RuntimeError, ~r/Custom error/, fn -> + get_dotenv( + "TEST_VALUE", + fn _ -> + raise Dotenvy.Error, message: "Custom error" + end, + "default" + ) + end + end + + test "raising other error types passes thru", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise FunctionClauseError, fn -> + get_dotenv("TEST_VALUE", fn _ -> Keyword.get(%{}, :foo) end, "default") + end + end + end + + describe "fetch_dotenv!/2" do + test "wraps env!/2", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert env!("TEST_VALUE") == fetch_dotenv!("TEST_VALUE") + assert env!("TEST_VALUE", :string) == fetch_dotenv!("TEST_VALUE", :string) + end + + # Returns the same results as `env!/2` for non-happy-path tests + test "raises when variable not set" do + assert_raise RuntimeError, fn -> + fetch_dotenv!("DOES_NOT_EXIST", :string!) + end + end + + test "built-in conversion errors convert to RuntimeError", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise RuntimeError, fn -> + fetch_dotenv!("TEST_VALUE", :integer) + end + end + + test "raising Dotenvy.Error with custom message converts to RuntimeError", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise RuntimeError, ~r/Custom error/, fn -> + fetch_dotenv!("TEST_VALUE", fn _ -> + raise Dotenvy.Error, message: "Custom error" + end) + end + end + + test "raising other error types passes thru", %{test: test} do + System.put_env("TEST_VALUE", "#{test}") + source([System.get_env()]) + + assert_raise FunctionClauseError, fn -> + fetch_dotenv!("TEST_VALUE", fn _ -> Keyword.get(%{}, :foo) end) + end + end + end + describe "source/2" do test ":ok when no files parsed" do assert {:ok, %{}} == source("does_not_exist")