-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Expand file tree
/
Copy pathconfig.ex
More file actions
221 lines (175 loc) · 5.36 KB
/
config.ex
File metadata and controls
221 lines (175 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
defmodule Mix.Config do
@moduledoc ~S"""
Module for defining, reading and merging app configurations.
Most commonly, this module is used to define your own configuration:
use Mix.Config
config :plug,
key1: "value1",
key2: "value2"
import_config "#{Mix.env}.exs"
All `config/*` macros, including `import_config/1`, are used
to help define such configuration files.
Furthermore, this module provides functions like `read!/1`,
`merge/2` and friends which help manipulate configurations
in general.
"""
defmodule LoadError do
defexception [:file, :error]
def message(%LoadError{file: file, error: error}) do
"could not load config #{Path.relative_to_cwd(file)}\n " <>
"#{Exception.format_banner(:error, error)}"
end
end
@doc false
defmacro __using__(_) do
quote do
import Mix.Config, only: [config: 2, config: 3, import_config: 1]
var!(config, Mix.Config) = []
end
end
@doc """
Configures the given application.
Keyword lists are always deep merged.
## Examples
The given `opts` are merged into the existing configuration
for the given `app`. Conflicting keys are overridden by the
ones specified in `opts`. For example, the declaration below:
config :lager,
log_level: :warn,
mode: :truncate
config :lager,
log_level: :info,
threshold: 1024
Will have a final configuration of:
[log_level: :info, mode: :truncate, threshold: 1024]
"""
defmacro config(app, opts) do
quote do
var!(config, Mix.Config) =
Mix.Config.merge(var!(config, Mix.Config), [{unquote(app), unquote(opts)}])
end
end
@doc """
Configures the given key for the given application.
Keyword lists are always deep merged.
## Examples
The given `opts` are merged into the existing values for `key`
in the given `app`. Conflicting keys are overridden by the
ones specified in `opts`. For example, the declaration below:
config :ecto, Repo,
log_level: :warn
config :ecto, Repo,
log_level: :info,
pool_size: 10
Will have a final value for `Repo` of:
[log_level: :info, pool_size: 10]
"""
defmacro config(app, key, opts) do
quote do
var!(config, Mix.Config) =
Mix.Config.merge(var!(config, Mix.Config),
[{unquote(app), [{unquote(key), unquote(opts)}]}])
end
end
@doc ~S"""
Imports configuration from the given file.
The path is expected to be relative to the directory the
current configuration file is on.
## Examples
This is often used to emulate configuration across environments:
import_config "#{Mix.env}.exs"
Or to import files from children in umbrella projects:
import_config "../apps/*/config/config.exs"
"""
defmacro import_config(file) do
quote do
var!(config, Mix.Config) =
Mix.Config.read_wildcard!(var!(config, Mix.Config), Path.expand(unquote(file), __DIR__))
end
end
@doc """
Reads and validates a configuration file.
"""
def read!(file) do
try do
{config, binding} = Code.eval_file(file)
config =
case List.keyfind(binding, {:config, Mix.Config}, 0) do
{_, value} -> value
nil -> config
end
validate!(config)
config
rescue
e in [LoadError] -> reraise(e, System.stacktrace)
e -> reraise(LoadError, [file: file, error: e], System.stacktrace)
end
end
@doc """
Reads many configuration files given by wildcard into a single config.
Raises an error if `path` is a concrete filename (with no wildcards)
but the corresponding file does not exist.
"""
def read_wildcard!(config, path) do
paths = if String.contains?(path, ~w(* ? [ {))do
Path.wildcard(path)
else
[path]
end
Enum.reduce(paths, config, &merge(&2, read!(&1)))
end
@doc """
Persists the given configuration by modifying
the configured applications environment.
"""
def persist(config) do
for {app, kw} <- config, {k, v} <- kw do
:application.set_env(app, k, v, persistent: true)
end
:ok
end
@doc """
Validates a configuration.
"""
def validate!(config) do
if is_list(config) do
Enum.all?(config, fn
{app, value} when is_atom(app) ->
if Keyword.keyword?(value) do
true
else
raise ArgumentError,
"expected config for app #{inspect app} to return keyword list, got: #{inspect value}"
end
_ ->
false
end)
else
raise ArgumentError,
"expected config file to return keyword list, got: #{inspect config}"
end
end
@doc """
Merges two configurations.
The configuration of each application is merged together
with the values in the second one having higher preference
than the first in case of conflicts.
## Examples
iex> Mix.Config.merge([app: [k: :v1]], [app: [k: :v2]])
[app: [k: :v2]]
iex> Mix.Config.merge([app1: []], [app2: []])
[app1: [], app2: []]
"""
def merge(config1, config2) do
Keyword.merge(config1, config2, fn _, app1, app2 ->
Keyword.merge(app1, app2, &deep_merge/3)
end)
end
defp deep_merge(_key, value1, value2) do
if Keyword.keyword?(value1) and Keyword.keyword?(value2) do
Keyword.merge(value1, value2, &deep_merge/3)
else
value2
end
end
end