|
| 1 | +defmodule AppBootstrap do |
| 2 | + @switches [ |
| 3 | + strict: [app: :string, env: :string, out: :string], |
| 4 | + aliases: [a: :app, e: :env, o: :out] |
| 5 | + ] |
| 6 | + |
| 7 | + def main(argv) do |
| 8 | + with {:ok, parsed} <- parse_args(argv), |
| 9 | + {:ok, app_env} <- read_app_env(parsed[:app] || "./app.json"), |
| 10 | + {:ok, local_env} <- read_local_env(parsed[:env] || "./.env"), |
| 11 | + new_dotenv = merge_env(local_env, app_env) |> format_dotenv, |
| 12 | + :ok <- File.write(parsed[:out], new_dotenv) do |
| 13 | + IO.puts "New env written to #{parsed[:out]}" |
| 14 | + exit(:normal) |
| 15 | + else |
| 16 | + {:error, invalid} -> |
| 17 | + exit(invalid) |
| 18 | + end |
| 19 | + end |
| 20 | + |
| 21 | + @spec parse_args(OptionParser.argv) :: |
| 22 | + {:ok, OptionParser.parsed} | {:error, String.t} |
| 23 | + defp parse_args(argv) do |
| 24 | + case OptionParser.parse(argv, @switches) do |
| 25 | + {parsed, _, []} -> {:ok, parsed} |
| 26 | + {_, _, invalid} -> {:error, "Invalid arguments passed"} |
| 27 | + end |
| 28 | + end |
| 29 | + |
| 30 | + @spec read_app_env(String.t) :: {:ok, map} | {:error, String.t} |
| 31 | + defp read_app_env(app_json_path) do |
| 32 | + with {:ok, json} <- File.read(app_json_path), |
| 33 | + {:ok, map} <- Poison.decode(json), |
| 34 | + env when is_map(env) <- Map.get(map, "env") do |
| 35 | + {:ok, env} |
| 36 | + else |
| 37 | + _ -> {:error, ~s(No "env" found in app.json)} |
| 38 | + end |
| 39 | + end |
| 40 | + |
| 41 | + @spec read_local_env(String.t) :: {:ok, map} | {:error, String.t} |
| 42 | + defp read_local_env(env_path) do |
| 43 | + env_string = |
| 44 | + case File.read(env_path) do |
| 45 | + {:ok, env_string} -> env_string |
| 46 | + {:error, _} -> "" |
| 47 | + end |
| 48 | + |
| 49 | + parse_dotenv(env_string) |
| 50 | + end |
| 51 | + |
| 52 | + @spec parse_dotenv(String.t) :: {:ok, map} | {:error, String.t} |
| 53 | + defp parse_dotenv(env_string) do |
| 54 | + env_string |
| 55 | + |> String.split("\n") |
| 56 | + |> Enum.reduce_while({:ok, %{}}, fn line, {:ok, dotenv} -> |
| 57 | + case line do |
| 58 | + "" -> |
| 59 | + {:cont, {:ok, dotenv}} |
| 60 | + value_line -> |
| 61 | + case parse_line(line) do |
| 62 | + {:ok, {key, value}} -> |
| 63 | + {:cont, {:ok, Map.put(dotenv, key, value)}} |
| 64 | + error -> |
| 65 | + {:halt, error} |
| 66 | + end |
| 67 | + end |
| 68 | + end) |
| 69 | + end |
| 70 | + |
| 71 | + @spec parse_line(String.t) :: {:ok, {String.t, String.t}} | {:error, String.t} |
| 72 | + defp parse_line(line) do |
| 73 | + case String.split(line, "=", parts: 2) do |
| 74 | + [key, value] -> |
| 75 | + {:ok, {key, strip_quotes(value)}} |
| 76 | + line -> |
| 77 | + {:error, ~s(Could not parse dotenv line: #{line})} |
| 78 | + end |
| 79 | + end |
| 80 | + |
| 81 | + @spec strip_quotes(String.t) :: String.t |
| 82 | + defp strip_quotes(value) do |
| 83 | + if String.starts_with?(value, ~s(")) and String.ends_with?(value, ~s(")) do |
| 84 | + String.slice(value, 1..-2) |
| 85 | + else |
| 86 | + value |
| 87 | + end |
| 88 | + end |
| 89 | + |
| 90 | + @spec merge_env(map, map) :: String.t |
| 91 | + defp merge_env(local, app) do |
| 92 | + Enum.reduce(app, local, fn ({key, descriptor}, local) -> |
| 93 | + case Map.get(local, key) do |
| 94 | + value when not is_nil(value) -> |
| 95 | + local |
| 96 | + nil -> |
| 97 | + Map.put(local, key, get_value(key, descriptor)) |
| 98 | + end |
| 99 | + end) |
| 100 | + end |
| 101 | + |
| 102 | + @spec get_value(String.t, map) :: String.t |
| 103 | + defp get_value(key, descriptor) do |
| 104 | + case descriptor do |
| 105 | + %{"development_required" => false} -> "" |
| 106 | + %{"required" => false} -> "" |
| 107 | + %{"development_value" => value} -> value |
| 108 | + %{"value" => value} -> value |
| 109 | + %{"description" => description} -> |
| 110 | + IO.puts ~s(Provide a value for "#{key}":) |
| 111 | + IO.puts ~s("#{description}") |
| 112 | + IO.gets("➜ ") |> String.trim_trailing |
| 113 | + end |
| 114 | + end |
| 115 | + |
| 116 | + @spec format_dotenv(map) :: String.t |
| 117 | + defp format_dotenv(dotenv) do |
| 118 | + Enum.reduce(dotenv, "", fn ({key, value}, string) -> |
| 119 | + "#{string}#{key}=#{value}\n" |
| 120 | + end) |
| 121 | + end |
| 122 | +end |
0 commit comments