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
12 changes: 12 additions & 0 deletions docs/cheatsheets/cheatsheet.cheatmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions lib/dotenvy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 113 additions & 0 deletions test/dotenvy_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down