1defmodule Mix.Tasks.Hex.Config do
2  use Mix.Task
3
4  @shortdoc "Reads, updates or deletes local Hex config"
5
6  @moduledoc """
7  Reads, updates or deletes local Hex config.
8
9  ## List config keys and values
10
11      $ mix hex.config
12
13  ## Get or delete config value for KEY
14
15      $ mix hex.config KEY [--delete]
16
17  ## Set config KEY to VALUE
18
19      $ mix hex.config KEY VALUE
20
21  ## Config keys
22
23    * `api_key` - Your API key. If you are authenticated this config will override
24      the API key used for your authenticated user. Can be also be overridden by
25      setting the environment variable `HEX_API_KEY`
26    * `api_url` - Hex API URL. Can be overridden by setting the environment
27      variable `HEX_API_URL` (Default: `#{inspect(Hex.State.default_api_url())}`)
28    * `offline` - If set to true Hex will not fetch the registry or packages and
29      will instead use locally cached files if they are available. Can be
30      overridden by setting the environment variable `HEX_OFFLINE` (Default:
31      `false`)
32    * `unsafe_https` - If set to true Hex will not verify HTTPS certificates.
33      Can be overridden by setting the environment variable `HEX_UNSAFE_HTTPS`
34      (Default: `false`)
35    * `unsafe_registry` - If set to true Hex will not verify the registry
36      signature against the repository's public key. Can be overridden by
37      setting the environment variable `HEX_UNSAFE_REGISTRY` (Default:
38      `false`)
39    * `no_verify_repo_origin` - If set to true Hex will not verify the registry
40      origin. Can be overridden by setting the environment variable
41      `HEX_NO_VERIFY_REPO_ORIGIN` (Default: `false`)
42    * `http_proxy` - HTTP proxy server. Can be overridden by setting the
43      environment variable `HTTP_PROXY` (Default: `nil`)
44    * `https_proxy` - HTTPS proxy server. Can be overridden by setting the
45      environment variable `HTTPS_PROXY` (Default: `nil`)
46    * `no_proxy` - A comma separated list of hostnames that will not be proxied,
47      asterisks can be used as wildcards. Can be overridden by setting the
48      environment variable `no_proxy` or `NO_PROXY` (Default: `nil`)
49    * `http_concurrency` - Limits the number of concurrent HTTP requests in
50      flight. Can be overridden by setting the environment variable
51      `HEX_HTTP_CONCURRENCY` (Default: `8`)
52    * `http_timeout` - Sets the timeout for HTTP requests in seconds. Can be
53      overridden by setting the environment variable `HEX_HTTP_TIMEOUT`
54      (Default: `nil`)
55    * `mirror_url` - Hex mirror URL. Can be overridden by setting the
56      environment variable `HEX_MIRROR` (Default: `nil`)
57    * `cacerts_path` - Path to the CA certificate store PEM file. If not set,
58      a CA bundle that ships with Hex is used. Can be overridden by setting the
59      environment variable `HEX_CACERTS_PATH`. (Default: `nil`)
60    * `no_short_urls` - If set to true Hex will not
61      shorten any links. Can be overridden by setting the environment variable
62      `HEX_NO_SHORT_URLS` (Default: `false`)
63
64  Hex responds to these additional environment variables:
65
66    * `HEX_HOME` - directory where Hex stores the cache and configuration
67      (Default: `~/.hex`)
68
69    * `MIX_XDG` - asks Hex to follow the [XDG Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
70      for its home directory and configuration files. `HEX_HOME` has higher preference
71      than `MIX_XDG`. If none of the variables are set, the default directory
72      `~/.hex` will be used.
73
74  ## Config overrides
75
76  All keys from the "Config keys" section above can be overridden.
77
78  Hex uses the following order of precedence when computing a value for a given key:
79
80    1. System environment
81
82       Setting for example `HEX_API_URL` environment variable has always the
83       highest precedence for the `api_url` config key.
84
85    2. Project configuration
86
87       Hex allows an optional, per-project configuration in the `mix.exs` file.
88
89       For example, to override `api_url` config key, add the following:
90
91           # mix.exs
92           defmodule MyApp.MixProject
93             def project() do
94               [
95                 # ...
96                 deps: deps(),
97                 hex: hex()
98               ]
99             end
100
101             defp hex() do
102               [
103                 api_url: "https://hex.myorg/api"
104               ]
105             end
106           end
107
108    3. Global configuration using `mix hex.config KEY VALUE`
109
110    4. Default value
111
112  ## Command line options
113
114    * `--delete` - Remove a specific config key
115  """
116  @behaviour Hex.Mix.TaskDescription
117
118  @switches [delete: :boolean]
119
120  @impl true
121  def run(args) do
122    Hex.start()
123    {opts, args} = Hex.OptionParser.parse!(args, strict: @switches)
124
125    case args do
126      [] ->
127        list()
128
129      ["$" <> _key | _] ->
130        Mix.raise("Invalid key name")
131
132      [key] ->
133        if opts[:delete] do
134          delete(key)
135        else
136          read(key)
137        end
138
139      [key, value] ->
140        set(key, value)
141
142      _ ->
143        Mix.raise("""
144        Invalid arguments, expected:
145
146        mix hex.config KEY [VALUE]
147        """)
148    end
149  end
150
151  @impl true
152  def tasks() do
153    [
154      {"", "Reads, updates or deletes local Hex config"},
155      {"KEY", "Get config value for KEY"},
156      {"KEY --delete", "Delete config value for KEY"},
157      {"KEY VALUE", "Set config KEY to VALUE"}
158    ]
159  end
160
161  defp list() do
162    Enum.each(valid_read_keys(), fn {config, _internal} ->
163      read(config, true)
164    end)
165  end
166
167  defp read(key, verbose \\ false)
168
169  defp read(key, verbose) when is_binary(key) do
170    key = String.to_atom(key)
171
172    case Keyword.fetch(valid_read_keys(), key) do
173      {:ok, internal} ->
174        fetch_current_value_and_print(internal, key, verbose)
175
176      _error ->
177        Mix.raise("The key #{key} is not valid")
178    end
179  end
180
181  defp read(key, verbose) when is_atom(key), do: read(to_string(key), verbose)
182
183  defp fetch_current_value_and_print(internal, key, verbose) do
184    case Map.fetch(Hex.State.get_all(), internal) do
185      {:ok, {{:env, env_var}, value}} ->
186        print_value(key, value, verbose, "(using `#{env_var}`)")
187
188      {:ok, {{:global_config, _key}, value}} ->
189        print_value(key, value, verbose, "(using `#{config_path()}`)")
190
191      {:ok, {{:project_config, _key}, value}} ->
192        print_value(key, value, verbose, "(using `mix.exs`)")
193
194      {:ok, {{:env_path_join, {env_var, _prefix}}, value}} ->
195        print_value(key, value, verbose, "(using `#{env_var}`)")
196
197      {:ok, {kind, value}} when kind in [:default, :computed] ->
198        print_value(key, value, verbose, "(default)")
199
200      :error ->
201        Mix.raise("Config does not contain the key #{key}")
202    end
203  end
204
205  defp print_value(key, value, true, source),
206    do: Hex.Shell.info("#{key}: #{inspect(value, pretty: true)} #{source}")
207
208  defp print_value(_key, value, false, _source), do: Hex.Shell.info(inspect(value, pretty: true))
209
210  defp delete(key) do
211    key = String.to_atom(key)
212
213    if Keyword.has_key?(valid_write_keys(), key) do
214      Hex.Config.remove([key])
215    end
216  end
217
218  defp set(key, value) do
219    key = String.to_atom(key)
220
221    if Keyword.has_key?(valid_write_keys(), key) do
222      Hex.Config.update([{key, value}])
223    else
224      Mix.raise("Invalid key #{key}")
225    end
226  end
227
228  defp config_path() do
229    :config_home
230    |> Hex.State.fetch!()
231    |> Path.join("hex.config")
232  end
233
234  defp valid_keys() do
235    Enum.map(Hex.State.config(), fn {internal, map} ->
236      [config | _] = Map.get(map, :config, [nil])
237      [env | _] = Map.get(map, :env, [nil])
238
239      cond do
240        String.starts_with?(to_string(config), "$") -> {internal, config, :not_accessible}
241        is_nil(config) and not is_nil(env) -> {internal, config, :env_only}
242        is_nil(config) and is_nil(env) -> {internal, config, :read_only}
243        true -> {internal, config, :read_and_write}
244      end
245    end)
246  end
247
248  defp valid_read_keys() do
249    valid_keys()
250    |> Enum.map(fn {internal, config, access} ->
251      if access != :not_accessible, do: key_representation(internal, config)
252    end)
253    |> Enum.filter(&(&1 != nil))
254  end
255
256  defp valid_write_keys() do
257    valid_keys()
258    |> Enum.map(fn {internal, config, access} ->
259      if access == :read_and_write, do: key_representation(internal, config)
260    end)
261    |> Enum.filter(&(&1 != nil))
262  end
263
264  defp key_representation(internal, nil), do: {internal, internal}
265  defp key_representation(internal, config), do: {config, internal}
266end
267