diff --git a/README.md b/README.md index a36d163..9d5f53c 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Elixir 1.13+ ```elixir def deps do [ - {:mahaul, "~> 0.3.0"} + {:mahaul, "~> 0.4.0"} ] end ``` @@ -45,9 +45,13 @@ config :mahaul, mix_env: Mix.env() ```elixir defmodule MyApp.Env do use Mahaul, - PORT: [type: :port, default_dev: "4000"], - DEPLOYMENT_ENV: [type: :enum, choices: [:dev, :staging, :live], default_dev: "dev"], - DATABASE_URL: [type: :uri, default_dev: "postgresql://user:pass@localhost:5432/app_dev"], + DEPLOYMENT_ENV: [ + type: :enum, + defaults: [dev: "dev", test: "dev"], + choices: [:dev, :staging, :live] + ], + PORT: [type: :port, defaults: [dev: "4000"]], + DATABASE_URL: [type: :uri, defaults: [dev: "postgresql://user:pass@localhost:5432/app_dev"]], ANOTHER_ENV: [type: :host, default: "//localhost"] end ``` @@ -108,7 +112,7 @@ The following type configurations are supported. ## Setting Defaults -Any defaults and fallback values can be set globally using the `default` or for development environment using the `default_dev` configuration options. Make sure to use the string values same as we set in the actual system environment, as it will be parsed depending upon the provided `type` configuration. +Any defaults and fallback values can be set globally using the `default` or for any mix environment using the `defaults` configuration options. Make sure to use the string values same as we set in the actual system environment, as it will be parsed depending upon the provided `type` configuration. #### Globally @@ -129,48 +133,71 @@ iex> MyApp.Env.my_env() Hello Universe ``` -#### For dev/test environment +#### For any mix environment ```elixir defmodule MyApp.Env do use Mahaul, - MY_ENV: [type: :str, dev_default: "Hello Dev"] + MY_ENV: [ + type: :str, + defaults: [prod: "Hello Prod", dev: "Hello Dev", test: "Hello Test", custom: "Hello Custom"] + ] end ``` ``` +MIX_ENV=prod iex -S mix +iex> MyApp.Env.my_env() +Hello Prod + MIX_ENV=dev iex -S mix iex> MyApp.Env.my_env() Hello Dev -MIX_ENV=dev MY_ENV="Hello World" iex -S mix +MIX_ENV=test iex -S mix +iex> MyApp.Env.my_env() +Hello Test + +MIX_ENV=custom iex -S mix +iex> MyApp.Env.my_env() +Hello Custom + +MIX_ENV=prod MY_ENV="Hello World" iex -S mix iex> MyApp.Env.my_env() Hello World ``` -#### Globally with dev/test environment overrides +#### For any mix environment with fallback ```elixir defmodule MyApp.Env do use Mahaul, - MY_ENV: [type: :str, default: "Hello World", dev_default: "Hello Dev"] + MY_ENV: [ + type: :str, + default: "Hello World", + defaults: [prod: "Hello Prod", dev: "Hello Dev", test: "Hello Test"] + ] end ``` ``` +MIX_ENV=prod iex -S mix +iex> MyApp.Env.my_env() +Hello Prod + MIX_ENV=dev iex -S mix iex> MyApp.Env.my_env() Hello Dev -MIX_ENV=dev MY_ENV="Hello Universe" iex -S mix +MIX_ENV=test iex -S mix iex> MyApp.Env.my_env() -Hello Universe +Hello Test -MIX_ENV=prod iex -S mix +MIX_ENV=custom iex -S mix iex> MyApp.Env.my_env() Hello World -MIX_ENV=prod MY_ENV="Hello Universe" iex -S mix +MIX_ENV=custom MY_ENV="Hello Universe" iex -S mix iex> MyApp.Env.my_env() Hello Universe ``` @@ -220,6 +247,9 @@ git clone https://github.com/emadalam/mahaul.git cd mahaul mix deps.get mix test + +# or with coverage threshold +# mix coveralls ``` ### Building docs diff --git a/coveralls.json b/coveralls.json index 466c53f..04dc1b0 100644 --- a/coveralls.json +++ b/coveralls.json @@ -1,5 +1,5 @@ { "coverage_options": { - "minimum_coverage": 90 + "minimum_coverage": 95 } } \ No newline at end of file diff --git a/lib/mahaul.ex b/lib/mahaul.ex index de02bb3..205721c 100644 --- a/lib/mahaul.ex +++ b/lib/mahaul.ex @@ -2,7 +2,7 @@ defmodule Mahaul do @external_resource "README.md" @moduledoc File.read!("README.md") |> String.replace("# Mahaul\n\n", "", global: false) - @version "0.3.0" + @version "0.4.0" @doc false def version, do: @version diff --git a/lib/mahaul/helpers.ex b/lib/mahaul/helpers.ex index b40772a..541966f 100644 --- a/lib/mahaul/helpers.ex +++ b/lib/mahaul/helpers.ex @@ -77,12 +77,20 @@ defmodule Mahaul.Helpers do defp get_env_val_or_default(env_val, config, mix_env \\ get_mix_env()) - defp get_env_val_or_default(env_val, config, mix_env) when mix_env in [:dev, :test] do - env_val || Keyword.get(config, :default_dev) || Keyword.get(config, :default) - end - - defp get_env_val_or_default(env_val, config, _mix_env) do - env_val || Keyword.get(config, :default) + defp get_env_val_or_default(env_val, config, mix_env) do + # simplify this once we remove support for `:default_dev` option + # in favour of the `:defaults` option + if Keyword.has_key?(config, :default_dev) do + case mix_env do + env when env in [:dev, :test] -> + env_val || Keyword.get(config, :default_dev) || Keyword.get(config, :default) + + _ -> + env_val || Keyword.get(config, :default) + end + else + env_val || config[:defaults][mix_env] || Keyword.get(config, :default) + end end defp get_mix_env, do: Application.get_env(:mahaul, :mix_env, :prod) @@ -300,20 +308,29 @@ defmodule Mahaul.Helpers do end defp validate_opt!({:default_dev, default_dev}, name) do + IO.warn( + ~s(#{name}: :default_dev option is deprecated, use :defaults instead. eg: defaults: [prod: "MY_VAL1", dev: "MY_VAL2", test: "MY_VAL3"]), + Macro.Env.stacktrace(__ENV__) + ) + unless is_binary(default_dev) do raise ArgumentError, "#{name}: expected :default_dev to be a string, got: #{inspect(default_dev)}" end end + defp validate_opt!({:defaults, defaults}, name) do + validate_keyword!(defaults, name, ":defaults") + end + defp validate_opt!(option, name) do raise ArgumentError, "#{name}: unknown option provided #{inspect(option)}" end - defp validate_keyword!(opts, name \\ "Mahaul") do + defp validate_keyword!(opts, name \\ "Mahaul", opt_name \\ "options") do unless Keyword.keyword?(opts) and not Enum.empty?(opts) do raise ArgumentError, - "#{name}: expected options to be a non-empty keyword list, got: #{inspect(opts)}" + "#{name}: expected #{opt_name} to be a non-empty keyword list, got: #{inspect(opts)}" end end end diff --git a/mix.exs b/mix.exs index 316cbe4..83a0a35 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Mahaul.MixProject do use Mix.Project @source_url "https://github.com/emadalam/mahaul" - @version "0.3.0" + @version "0.4.0" def project do [ diff --git a/test/mahaul_test.exs b/test/mahaul_test.exs index 6f0e4b4..d2fc82d 100644 --- a/test/mahaul_test.exs +++ b/test/mahaul_test.exs @@ -2,6 +2,7 @@ defmodule MahaulTest do use ExUnit.Case, async: true import ExUnit.CaptureLog + import ExUnit.CaptureIO @env_list [ {"MOCK__ENV__STR", "__MOCK__VAL1__"}, @@ -35,6 +36,8 @@ defmodule MahaulTest do [MOCK__ENV: [type: :str, choices: true]]}, {"MOCK__ENV: expected :choices to be a non-empty list, got: []", [MOCK__ENV: [type: :str, choices: []]]}, + {"MOCK__ENV: expected :defaults to be a non-empty keyword list, got: []", + [MOCK__ENV: [type: :str, defaults: []]]}, {"MOCK__ENV: expected :default to be a string, got: 1000", [MOCK__ENV: [type: :str, default: 1000]]}, {"MOCK__ENV: expected :default_dev to be a string, got: false", @@ -45,15 +48,33 @@ defmodule MahaulTest do for {{error, opts}, index} <- invalid_opts_samples |> Enum.with_index() do test "should raise exception for invalid options #{inspect(opts)}" do - assert_raise ArgumentError, unquote(error), fn -> - defmodule String.to_atom("Env0.#{unquote(index)}") do - use Mahaul, unquote(opts) + fun = fn -> + assert_raise ArgumentError, unquote(error), fn -> + defmodule String.to_atom("Env0.#{unquote(index)}") do + use Mahaul, unquote(opts) + end end end + + capture_io(:stderr, fun) end end end + describe "deprecation warning" do + test "should warn on :default_dev option" do + fun = fn -> + defmodule Env0.Deprecated do + use Mahaul, + MOCK__ENV: [type: :str, default_dev: "__MOCK__"] + end + end + + assert capture_io(:stderr, fun) =~ + ~s(MOCK__ENV: :default_dev option is deprecated, use :defaults instead. eg: defaults: [prod: "MY_VAL1", dev: "MY_VAL2", test: "MY_VAL3"]) + end + end + describe "validate/0" do test "should return success for valid environment variables" do defmodule Env1 do @@ -89,14 +110,15 @@ defmodule MahaulTest do Env2.validate() end - capture_log(fun) =~ ~s(missing or invalid environment variables. - MOCK__ENV__MISSING - MOCK__ENV__NUM - MOCK__ENV__INT - MOCK__ENV__BOOL - MOCK__ENV__PORT - MOCK__ENV__HOST - MOCK__ENV__URI) + assert capture_log(fun) =~ + "missing or invalid environment variables.\n" <> + "MOCK__ENV__MISSING\n" <> + "MOCK__ENV__NUM\n" <> + "MOCK__ENV__INT\n" <> + "MOCK__ENV__BOOL\n" <> + "MOCK__ENV__PORT\n" <> + "MOCK__ENV__HOST\n" <> + "MOCK__ENV__URI" end end @@ -139,14 +161,15 @@ defmodule MahaulTest do end end - capture_log(fun) =~ ~s(missing or invalid environment variables. - MOCK__ENV__MISSING - MOCK__ENV__NUM - MOCK__ENV__INT - MOCK__ENV__BOOL - MOCK__ENV__PORT - MOCK__ENV__HOST - MOCK__ENV__URI) + assert capture_log(fun) =~ + "missing or invalid environment variables.\n" <> + "MOCK__ENV__MISSING\n" <> + "MOCK__ENV__NUM\n" <> + "MOCK__ENV__INT\n" <> + "MOCK__ENV__BOOL\n" <> + "MOCK__ENV__PORT\n" <> + "MOCK__ENV__HOST\n" <> + "MOCK__ENV__URI" end end @@ -197,106 +220,146 @@ defmodule MahaulTest do assert "ftp://example.com/something" = Env6.mock__env__new__uri() end - test "should return default values for prod" do + test "deprecated: should return default values for prod" do Config.Reader.read!("test/support/config/prod.exs") |> Application.put_all_env() - defmodule Env7 do - use Mahaul, - MOCK__ENV__NEW__STR: [type: :str, default: "VAL1"], - MOCK__ENV__NEW__ENUM: [type: :enum, default: "VAL2"], - MOCK__ENV__NEW__NUM: [type: :num, default: "101.11"], - MOCK__ENV__NEW__INT: [type: :int, default: "9876"], - MOCK__ENV__NEW__BOOL: [type: :bool, default: "1"], - MOCK__ENV__NEW__PORT: [type: :port, default: "4000"], - MOCK__ENV__NEW__HOST: [type: :host, default: "//192.168.0.1"], - MOCK__ENV__NEW__URI: [type: :uri, default: "ftp://example.com/something"] + fun = fn -> + defmodule Env7 do + use Mahaul, + MOCK__ENV__NEW__STR: [type: :str, default: "VAL1"], + MOCK__ENV__NEW__ENUM: [type: :enum, default: "VAL2"], + MOCK__ENV__NEW__NUM: [type: :num, default: "101.11"], + MOCK__ENV__NEW__INT: [type: :int, default: "9876"], + MOCK__ENV__NEW__BOOL: [type: :bool, default: "1"], + MOCK__ENV__NEW__PORT: [type: :port, default: "4000"], + MOCK__ENV__NEW__HOST: [type: :host, default: "//192.168.0.1"], + MOCK__ENV__NEW__URI: [type: :uri, default: "ftp://example.com/something"] + end + + assert "VAL1" = Env7.mock__env__new__str() + assert :VAL2 = Env7.mock__env__new__enum() + assert 101.11 = Env7.mock__env__new__num() + assert 9876 = Env7.mock__env__new__int() + assert true = Env7.mock__env__new__bool() + assert 4000 = Env7.mock__env__new__port() + assert "//192.168.0.1" = Env7.mock__env__new__host() + assert "ftp://example.com/something" = Env7.mock__env__new__uri() end - assert "VAL1" = Env7.mock__env__new__str() - assert :VAL2 = Env7.mock__env__new__enum() - assert 101.11 = Env7.mock__env__new__num() - assert 9876 = Env7.mock__env__new__int() - assert true = Env7.mock__env__new__bool() - assert 4000 = Env7.mock__env__new__port() - assert "//192.168.0.1" = Env7.mock__env__new__host() - assert "ftp://example.com/something" = Env7.mock__env__new__uri() + capture_io(:stderr, fun) end - test "should return default values for dev" do + test "deprecated: should return default values for dev" do Config.Reader.read!("test/support/config/dev.exs") |> Application.put_all_env() - defmodule Env8 do - use Mahaul, - MOCK__ENV__NEW__STR: [type: :str, default_dev: "VAL1"], - MOCK__ENV__NEW__ENUM: [type: :enum, default_dev: "VAL2"], - MOCK__ENV__NEW__NUM: [type: :num, default_dev: "101.11"], - MOCK__ENV__NEW__INT: [type: :int, default_dev: "9876"], - MOCK__ENV__NEW__BOOL: [type: :bool, default_dev: "1"], - MOCK__ENV__NEW__PORT: [type: :port, default_dev: "4000"], - MOCK__ENV__NEW__HOST: [type: :host, default_dev: "//192.168.0.1"], - MOCK__ENV__NEW__URI: [type: :uri, default_dev: "ftp://example.com/something"] + fun = fn -> + defmodule Env8 do + use Mahaul, + MOCK__ENV__NEW__STR: [type: :str, default_dev: "VAL1"], + MOCK__ENV__NEW__ENUM: [type: :enum, default_dev: "VAL2"], + MOCK__ENV__NEW__NUM: [type: :num, default_dev: "101.11"], + MOCK__ENV__NEW__INT: [type: :int, default_dev: "9876"], + MOCK__ENV__NEW__BOOL: [type: :bool, default_dev: "1"], + MOCK__ENV__NEW__PORT: [type: :port, default_dev: "4000"], + MOCK__ENV__NEW__HOST: [type: :host, default_dev: "//192.168.0.1"], + MOCK__ENV__NEW__URI: [type: :uri, default_dev: "ftp://example.com/something"] + end + + assert "VAL1" = Env8.mock__env__new__str() + assert :VAL2 = Env8.mock__env__new__enum() + assert 101.11 = Env8.mock__env__new__num() + assert 9876 = Env8.mock__env__new__int() + assert true = Env8.mock__env__new__bool() + assert 4000 = Env8.mock__env__new__port() + assert "//192.168.0.1" = Env8.mock__env__new__host() + assert "ftp://example.com/something" = Env8.mock__env__new__uri() end - assert "VAL1" = Env8.mock__env__new__str() - assert :VAL2 = Env8.mock__env__new__enum() - assert 101.11 = Env8.mock__env__new__num() - assert 9876 = Env8.mock__env__new__int() - assert true = Env8.mock__env__new__bool() - assert 4000 = Env8.mock__env__new__port() - assert "//192.168.0.1" = Env8.mock__env__new__host() - assert "ftp://example.com/something" = Env8.mock__env__new__uri() + capture_io(:stderr, fun) end - test "should return default values for test" do + test "deprecated: should return default values for test" do Config.Reader.read!("test/support/config/test.exs") |> Application.put_all_env() - defmodule Env9 do - use Mahaul, - MOCK__ENV__NEW__STR: [type: :str, default_dev: "VAL1"], - MOCK__ENV__NEW__ENUM: [type: :enum, default_dev: "VAL2"], - MOCK__ENV__NEW__NUM: [type: :num, default_dev: "101.11"], - MOCK__ENV__NEW__INT: [type: :int, default_dev: "9876"], - MOCK__ENV__NEW__BOOL: [type: :bool, default_dev: "1"], - MOCK__ENV__NEW__PORT: [type: :port, default_dev: "4000"], - MOCK__ENV__NEW__HOST: [type: :host, default_dev: "//192.168.0.1"], - MOCK__ENV__NEW__URI: [type: :uri, default_dev: "ftp://example.com/something"] + fun = fn -> + defmodule Env9 do + use Mahaul, + MOCK__ENV__NEW__STR: [type: :str, default_dev: "VAL1"], + MOCK__ENV__NEW__ENUM: [type: :enum, default_dev: "VAL2"], + MOCK__ENV__NEW__NUM: [type: :num, default_dev: "101.11"], + MOCK__ENV__NEW__INT: [type: :int, default_dev: "9876"], + MOCK__ENV__NEW__BOOL: [type: :bool, default_dev: "1"], + MOCK__ENV__NEW__PORT: [type: :port, default_dev: "4000"], + MOCK__ENV__NEW__HOST: [type: :host, default_dev: "//192.168.0.1"], + MOCK__ENV__NEW__URI: [type: :uri, default_dev: "ftp://example.com/something"] + end + + assert "VAL1" = Env9.mock__env__new__str() + assert :VAL2 = Env9.mock__env__new__enum() + assert 101.11 = Env9.mock__env__new__num() + assert 9876 = Env9.mock__env__new__int() + assert true = Env9.mock__env__new__bool() + assert 4000 = Env9.mock__env__new__port() + assert "//192.168.0.1" = Env9.mock__env__new__host() + assert "ftp://example.com/something" = Env9.mock__env__new__uri() end - assert "VAL1" = Env9.mock__env__new__str() - assert :VAL2 = Env9.mock__env__new__enum() - assert 101.11 = Env9.mock__env__new__num() - assert 9876 = Env9.mock__env__new__int() - assert true = Env9.mock__env__new__bool() - assert 4000 = Env9.mock__env__new__port() - assert "//192.168.0.1" = Env9.mock__env__new__host() - assert "ftp://example.com/something" = Env9.mock__env__new__uri() + capture_io(:stderr, fun) end - test "should return default values with fallback for dev" do + test "deprecated: should return default values with fallback for dev" do Config.Reader.read!("test/support/config/dev.exs") |> Application.put_all_env() - defmodule Env10 do - use Mahaul, - MOCK__ENV__NEW__STR: [type: :str, default: "VAL1", default_dev: "DEV_VAL1"] + fun = fn -> + defmodule Env10 do + use Mahaul, + MOCK__ENV__NEW__STR: [type: :str, default: "VAL1", default_dev: "DEV_VAL1"] + end + + assert "DEV_VAL1" = Env10.mock__env__new__str() end - assert "DEV_VAL1" = Env10.mock__env__new__str() + capture_io(:stderr, fun) end - test "should not return default dev fallback values for prod" do + test "deprecated: should not return default dev fallback values for prod" do Config.Reader.read!("test/support/config/prod.exs") |> Application.put_all_env() - defmodule Env11 do + fun = fn -> + defmodule Env11 do + use Mahaul, + MOCK__ENV__NEW__STR: [type: :str, default: "VAL1", default_dev: "DEV_VAL1"] + end + + assert "VAL1" = Env11.mock__env__new__str() + end + + capture_io(:stderr, fun) + end + + test "should return mix environment specific defaults with fallback" do + Config.Reader.read!("test/support/config/custom.exs") + |> Application.put_all_env() + + defmodule Env12 do use Mahaul, - MOCK__ENV__NEW__STR: [type: :str, default: "VAL1", default_dev: "DEV_VAL1"] + MOCK__ENV__STR: [type: :str, default: "VAL1", defaults: [custom: "CUSTOM_VAL"]], + MOCK__ENV__NEW__STR: [type: :str, default: "VAL1", defaults: [custom: "CUSTOM_VAL"]], + MOCK__ENV__NEW__STR2: [ + type: :str, + default: "VAL1", + defaults: [dev: "DEV_VAL", prod: "PROD_VAL"] + ] end - assert "VAL1" = Env11.mock__env__new__str() + assert "__MOCK__VAL1__" = Env12.mock__env__str() + assert "CUSTOM_VAL" = Env12.mock__env__new__str() + assert "VAL1" = Env12.mock__env__new__str2() end end end diff --git a/test/support/config/custom.exs b/test/support/config/custom.exs new file mode 100644 index 0000000..02237ce --- /dev/null +++ b/test/support/config/custom.exs @@ -0,0 +1,3 @@ +import Config + +config :mahaul, mix_env: :custom