Skip to content

Commit cbef7e9

Browse files
author
José Valim
committed
Initial work on config
1 parent a184b40 commit cbef7e9

File tree

7 files changed

+294
-0
lines changed

7 files changed

+294
-0
lines changed

lib/mix/lib/mix/config.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
defmodule Mix.Config do
2+
@moduledoc """
3+
Module for reading and merging app configurations.
4+
"""
5+
6+
@doc """
7+
Reads a configuration file.
8+
9+
It returns the read configuration and a list of
10+
dependencies this configuration may have on.
11+
"""
12+
def read(file) do
13+
config = Code.eval_file(file) |> elem(0)
14+
validate!(config)
15+
config
16+
end
17+
18+
@doc """
19+
Validates a configuration.
20+
"""
21+
def validate!(config) do
22+
if is_list(config) do
23+
Enum.all?(config, fn
24+
{app, value} when is_atom(app) ->
25+
if Keyword.keyword?(value) do
26+
true
27+
else
28+
raise ArgumentError, message:
29+
"expected config for app #{inspect app} to return keyword list, got: #{inspect value}"
30+
end
31+
_ ->
32+
false
33+
end)
34+
else
35+
raise ArgumentError, message:
36+
"expected config to return keyword list, got: #{inspect config}"
37+
end
38+
end
39+
40+
@doc """
41+
Merges two configurations.
42+
43+
The configuration of each application is merged together
44+
with the values in the second one having higher preference
45+
than the first in case of conflicts.
46+
47+
## Examples
48+
49+
iex> Mix.Config.merge([app: [k: :v1]], [app: [k: :v2]])
50+
[app: [k: :v2]]
51+
52+
iex> Mix.Config.merge([app1: []], [app2: []])
53+
[app1: [], app2: []]
54+
55+
"""
56+
def merge(config1, config2) do
57+
Keyword.merge(config1, config2, fn _, app1, app2 ->
58+
Keyword.merge(app1, app2)
59+
end)
60+
end
61+
62+
@doc """
63+
Merges two configurations.
64+
65+
The configuration of each application is merged together
66+
and a callback is invoked in case of conflicts receiving
67+
the app, the conflicting key and both values. It must return
68+
a value that will be used as part of the conflict resolution.
69+
70+
## Examples
71+
72+
iex> Mix.Config.merge([app: [k: :v1]], [app: [k: :v2]],
73+
...> fn app, k, v1, v2 -> {app, k, v1, v2} end)
74+
[app: [k: {:app, :k, :v1, :v2}]]
75+
76+
"""
77+
def merge(config1, config2, callback) do
78+
Keyword.merge(config1, config2, fn app, app1, app2 ->
79+
Keyword.merge(app1, app2, fn k, v1, v2 ->
80+
if v1 == v2 do
81+
v1
82+
else
83+
callback.(app, k, v1, v2)
84+
end
85+
end)
86+
end)
87+
end
88+
end

lib/mix/lib/mix/tasks/loadconfig.ex

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
defmodule Mix.Tasks.Loadconfig do
2+
use Mix.Task
3+
4+
@moduledoc """
5+
Loads the application configuration.
6+
7+
In case the application is an umbrella application, the
8+
configuration for all children app will be merged together
9+
and, in case there are any conflicts, they need to be resolved
10+
in the umbrella application.
11+
"""
12+
13+
def run(_) do
14+
set load()
15+
end
16+
17+
def load() do
18+
if Mix.Project.get do
19+
project = Mix.Project.config
20+
project[:config_path]
21+
|> eval()
22+
|> merge(Mix.Project.umbrella?)
23+
else
24+
[]
25+
end
26+
end
27+
28+
def set(config) do
29+
_ =
30+
for {app, kw} <- config, {k, v} <- kw do
31+
:application.set_env(app, k, v, persist: true)
32+
end
33+
end
34+
35+
defp eval(nil) do
36+
if File.regular?("config/config.exs") do
37+
eval("config/config.exs")
38+
else
39+
[]
40+
end
41+
end
42+
43+
defp eval(file) do
44+
try do
45+
Mix.Config.read(file)
46+
catch
47+
kind, reason ->
48+
stacktrace = System.stacktrace
49+
Mix.shell.error "Could not load config #{file} from project #{inspect Mix.Project.get}"
50+
:erlang.raise(kind, reason, stacktrace)
51+
end
52+
end
53+
54+
defp merge(base, false), do: base
55+
defp merge(base, true) do
56+
Mix.Dep.Umbrella.unloaded
57+
|> Enum.reduce([], fn dep, acc ->
58+
Mix.Dep.in_dependency dep, fn _ ->
59+
config = eval(Mix.Project.config[:config_path])
60+
merge dep, acc, config, base
61+
end
62+
end)
63+
|> Mix.Config.merge(base)
64+
end
65+
66+
defp merge(dep, acc, config, base) do
67+
Mix.Config.merge(acc, config, fn app, k, v1, v2 ->
68+
if Keyword.has_key?(base, app) and Keyword.has_key?(base[app], k) do
69+
v1
70+
else
71+
raise Mix.Error, message: "umbrella child #{inspect dep.app} has set the configuration for " <>
72+
"key #{inspect k} in app #{inspect app} to #{inspect v2} but another umbrella child has " <>
73+
"already set it to #{inspect v1}. You need to remove the configuration or resolve " <>
74+
"the conflict by setting a value in the umbrella config"
75+
end
76+
end)
77+
end
78+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[sample: :oops]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:oops
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[my_app: [key: :value]]

lib/mix/test/mix/config_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Code.require_file "../test_helper.exs", __DIR__
2+
3+
defmodule Mix.ConfigTest do
4+
use MixTest.Case, async: true
5+
6+
doctest Mix.Config
7+
8+
test "read/1" do
9+
assert Mix.Config.read(fixture_path("configs/good.exs")) ==
10+
[my_app: [key: :value]]
11+
12+
msg = "expected config for app :sample to return keyword list, got: :oops"
13+
assert_raise ArgumentError, msg, fn ->
14+
Mix.Config.read fixture_path("configs/bad_app.exs")
15+
end
16+
17+
msg = "expected config to return keyword list, got: :oops"
18+
assert_raise ArgumentError, msg, fn ->
19+
Mix.Config.read fixture_path("configs/bad_root.exs")
20+
end
21+
end
22+
end
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
Code.require_file "../../test_helper.exs", __DIR__
2+
3+
defmodule Mix.Tasks.LoadconfigTest do
4+
use MixTest.Case
5+
6+
@apps [:my_app, :other_app]
7+
8+
teardown do
9+
Enum.each @apps, fn app ->
10+
Enum.each :application.get_all_env(app), fn {key, _} ->
11+
:application.unset_env(app, key, persist: true)
12+
end
13+
end
14+
:ok
15+
end
16+
17+
defmodule Config do
18+
def project do
19+
[app: :myconfig, version: "0.1.0"]
20+
end
21+
end
22+
23+
test "loads and sets application configuration" do
24+
Mix.Project.push Config
25+
26+
in_fixture "no_mixfile", fn ->
27+
write_config """
28+
[my_app: [key: :value]]
29+
"""
30+
31+
assert Application.fetch_env(:my_app, :key) == :error
32+
Mix.Tasks.Loadconfig.run []
33+
assert Application.fetch_env(:my_app, :key) == {:ok, :value}
34+
end
35+
end
36+
37+
test "logs on bad configuration" do
38+
Mix.Project.push Config
39+
40+
in_fixture "no_mixfile", fn ->
41+
write_config """
42+
:oops
43+
"""
44+
45+
assert_raise ArgumentError, "expected config to return keyword list, got: :oops", fn ->
46+
Mix.Tasks.Loadconfig.run []
47+
end
48+
49+
msg = "Could not load config config/config.exs from project #{inspect Config}"
50+
assert_received {:mix_shell, :error, [^msg]}
51+
end
52+
end
53+
54+
test "merge umbrella children configs" do
55+
in_fixture "umbrella_dep/deps/umbrella", fn ->
56+
Mix.Project.in_project(:umbrella, ".", fn _ ->
57+
write_config "apps/foo/config/config.exs", """
58+
[my_app: [key: :value]]
59+
"""
60+
61+
write_config "apps/bar/config/config.exs", """
62+
[other_app: [key: :value]]
63+
"""
64+
65+
Mix.Tasks.Loadconfig.run []
66+
assert Application.fetch_env(:my_app, :key) == {:ok, :value}
67+
assert Application.fetch_env(:other_app, :key) == {:ok, :value}
68+
end)
69+
end
70+
end
71+
72+
test "raises on umbrella conflicts until resolved" do
73+
in_fixture "umbrella_dep/deps/umbrella", fn ->
74+
Mix.Project.in_project(:umbrella, ".", fn _ ->
75+
write_config "apps/foo/config/config.exs", """
76+
[my_app: [key: :value1]]
77+
"""
78+
79+
write_config "apps/bar/config/config.exs", """
80+
[my_app: [key: :value2]]
81+
"""
82+
83+
msg = ~r":foo has set the configuration for key :key in app :my_app to :value1"
84+
assert_raise Mix.Error, msg, fn ->
85+
Mix.Tasks.Loadconfig.run []
86+
end
87+
88+
write_config """
89+
[my_app: [key: :value3], other_app: [key: :value]]
90+
"""
91+
92+
Mix.Tasks.Loadconfig.run []
93+
assert Application.fetch_env(:other_app, :key) == {:ok, :value}
94+
assert Application.fetch_env(:my_app, :key) == {:ok, :value3}
95+
end)
96+
end
97+
end
98+
99+
defp write_config(path \\ "config/config.exs", contents) do
100+
File.mkdir_p! Path.dirname(path)
101+
File.write! path, contents
102+
end
103+
end

0 commit comments

Comments
 (0)