initial implementation

This commit is contained in:
sloane 2024-09-23 20:59:32 -04:00
commit fca37813ba
Signed by: sloanelybutsurely
SSH key fingerprint: SHA256:8SBnwhl+RY3oEyQxy1a9wByPzxWM0x+/Ejc+sIlY5qQ
7 changed files with 181 additions and 0 deletions

4
.formatter.exs Normal file
View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

26
.gitignore vendored Normal file
View file

@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
envars-*.tar
# Temporary files, for example, from tests.
/tmp/

21
README.md Normal file
View file

@ -0,0 +1,21 @@
# Envars
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `envars` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:envars, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/envars>.

58
lib/envars.ex Normal file
View file

@ -0,0 +1,58 @@
defmodule Envars do
@moduledoc """
`Envars` provides a convenience function to fetch and parse environment
variables all in once place. This has the benefit of also providing an error
message listing all missing required values.
## Example
%{
"PORT" => port,
"ENABLE_SSL" => enable_ssl,
"AWS_ACCESS_KEY_ID" => access_key_id,
"PHX_HOST" => phx_host
} = Envars.read!(%{
"PORT" => [type: :integer, required: false, default: 4000],
"ENABLE_SSL" => [type: :boolean],
"AWS_ACCESS_KEY_ID" => [type: :string],
"PHX_HOST" => [type: :string]
})
"""
@type field_type :: :string | :integer | :boolean
@type options :: [
{:type, field_type()} | {:required, boolean()} | {:default, term()}
]
@spec read!(fields :: %{(field :: String.t()) => options()}) :: %{String.t() => term()}
def read!(fields) do
{valid, invalid} =
for {field, opts} <- fields do
type = Keyword.get(opts, :type, :string)
required = Keyword.get(opts, :required, true)
default = Keyword.get(opts, :default)
value = System.get_env(field)
case {value, required, default} do
{nil, true, nil} -> {:error, {field, :undefined}}
{nil, _, default} -> {:ok, {field, default}}
{value, _, _} -> {:ok, {field, parse(type, value)}}
end
end
|> Enum.split_with(&(elem(&1, 0) == :ok))
if Enum.empty?(invalid) do
for {:ok, {field, value}} <- valid, into: %{}, do: {field, value}
else
missing_fields = for {:error, {field, _}} <- invalid, do: field
raise "Missing environment variables:\n\n - #{Enum.join(missing_fields, "\n - ")}"
end
end
defp parse(:string, value), do: value
defp parse(:integer, value), do: String.to_integer(value)
defp parse(:boolean, "0"), do: false
defp parse(:boolean, "1"), do: true
defp parse(:boolean, value), do: String.downcase(value) == "true"
end

28
mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Envars.MixProject do
use Mix.Project
def project do
[
app: :envars,
version: "0.1.0",
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

43
test/envars_test.exs Normal file
View file

@ -0,0 +1,43 @@
defmodule EnvarsTest do
use ExUnit.Case
describe "Envars.read!/1" do
setup do
System.put_env("STRING_VAR", "FOOBAR")
System.put_env("INTEGER_VAR", "123")
System.put_env("BOOLEAN_VAR_1", "1")
System.put_env("BOOLEAN_VAR_2", "0")
System.put_env("BOOLEAN_VAR_3", "True")
System.put_env("BOOLEAN_VAR_4", "FALSE")
end
test "parses environment variables and uses defaults" do
assert %{
"STRING_VAR" => "FOOBAR",
"INTEGER_VAR" => 123,
"UNDEFINED_VAR" => nil,
"BOOLEAN_VAR_1" => true,
"BOOLEAN_VAR_2" => false,
"BOOLEAN_VAR_3" => true,
"BOOLEAN_VAR_4" => false,
"DEFAULTED_VAR" => "<default value>"
} ==
Envars.read!(%{
"STRING_VAR" => [type: :string, required: true],
"INTEGER_VAR" => [type: :integer, default: 10],
"UNDEFINED_VAR" => [required: false],
"BOOLEAN_VAR_1" => [type: :boolean],
"BOOLEAN_VAR_2" => [type: :boolean],
"BOOLEAN_VAR_3" => [type: :boolean],
"BOOLEAN_VAR_4" => [type: :boolean],
"DEFAULTED_VAR" => [required: true, default: "<default value>"]
})
end
test "raises and error with ALL missing environment variables" do
assert_raise RuntimeError, ~r/MISSING_1\n - MISSING_2/, fn ->
Envars.read!(%{"MISSING_1" => [], "MISSING_2" => []})
end
end
end
end

1
test/test_helper.exs Normal file
View file

@ -0,0 +1 @@
ExUnit.start()