elixir-toniq / vapor

Runtime configuration system for Elixir
MIT License
596 stars 37 forks source link

Combining Env and File providers #108

Open yeraydiazdiaz opened 3 years ago

yeraydiazdiaz commented 3 years ago

Hi, thanks so much for Vapor and everything you do for the Elixir community.

In my app I was trying to define the configuration in a file but be able override some of it using environment variables. This, however, doesn't seem possible at the moment because both Env and File providers require all non-default bindings to be present. For example:

Having a config YAML file like:

HOST: localhost
PORT: 4000

and a setup like:

bindings = [
      {:host, "HOST"},
      {:port, "PORT"},
]
providers = [
    %Vapor.Provider.File{path: "config.yaml", bindings: bindings},
    %Vapor.Provider.Env{bindings: bindings},
]

Will fail if either HOST or PORT are not present in the environment variables.

Personally, I was expecting the combined results of all providers to raise if some of the bindings were missing. I realize that's probably not what the bindings were designed to do though, but it seems the only way to ensure that the config file includes all the required variables.

Thanks again 💯

lafka commented 3 years ago

Environment variables by default is set with required: true. If you use [{:host, "HOST", required: false},...] in your bindings this should probably work.

One consideration, the env will default to nil which overrides whatever was there in file. I've sucesfully used this patch to allow ENV to override previous variables:

diff --git a/lib/vapor/providers/env.ex b/lib/vapor/providers/env.ex
index cfee96f..6c27fdb 100644
--- a/lib/vapor/providers/env.ex
+++ b/lib/vapor/providers/env.ex
@@ -30,7 +30,7 @@ defmodule Vapor.Provider.Env do
       else
         envs =
           bound_envs
-          |> Enum.reject(fn {_, data} -> data.val == :missing end)
+          |> Enum.reject(fn {_, data} -> data.val in [:missing, :skip] end)
           |> Enum.map(fn {name, data} -> {name, data.val} end)
           |> Enum.into(%{})

@@ -51,7 +51,7 @@ defmodule Vapor.Provider.Env do
           val = if data.opts[:default] != nil do
             data.opts[:default]
           else
-            if data.opts[:required], do: :missing, else: nil
+            if data.opts[:required], do: :missing, else: :skip
           end
           {name, %{data | val: val}}

If you're using %Group{}, or the DSL, then the default loader will fail since it does not do recursive merge. Making a merge strategy that fits every use I assume is out of scope for this library. If you need this you can copy out Vapor.loader and make it recursive and then use something like this (instead of Vapor.load(Provider)):

      case CustomLoader.load(CustomConfigProvider) do
        {:ok, cfg} ->
          cfg

        {:error, errors} ->
          raise Vapor.LoadError.exception(errors)
      end
autodidaddict commented 3 years ago

I've run into this exact same problem. The use of "precedence" with file and env providers doesn't seem to work as advertised / expected. If I put a setting in a file, and then it shows up as a binding in the environment provider configuration, but that env var hasn't been set, then I should be able to use the file-sourced value. Today, if I toggle required: false then I get nil for the merged config value rather than what I had in the file.

Also, the file provider doesn't actually deal with missing files. If the file is missing, it should do nothing and move on to the next provider. Today, it crashes meaning I can't use the nice DSL because I have to drop a File.exists? in the mix.