aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/ct_formatter.ex130
-rw-r--r--lib/ejabberd/config/attr.ex119
-rw-r--r--lib/ejabberd/config/config.ex145
-rw-r--r--lib/ejabberd/config/ejabberd_hook.ex23
-rw-r--r--lib/ejabberd/config/ejabberd_module.ex69
-rw-r--r--lib/ejabberd/config/logger/ejabberd_logger.ex32
-rw-r--r--lib/ejabberd/config/opts_formatter.ex46
-rw-r--r--lib/ejabberd/config/store.ex55
-rw-r--r--lib/ejabberd/config/validator/validation.ex38
-rw-r--r--lib/ejabberd/config/validator/validator_attrs.ex28
-rw-r--r--lib/ejabberd/config/validator/validator_dependencies.ex30
-rw-r--r--lib/ejabberd/config/validator/validator_utility.ex30
-rw-r--r--lib/ejabberd/config_util.ex18
-rw-r--r--lib/ejabberd/module.ex19
-rw-r--r--lib/mix/tasks/deps.tree.ex94
-rw-r--r--lib/mod_presence_demo.ex17
16 files changed, 756 insertions, 137 deletions
diff --git a/lib/ct_formatter.ex b/lib/ct_formatter.ex
deleted file mode 100644
index 0c301353b..000000000
--- a/lib/ct_formatter.ex
+++ /dev/null
@@ -1,130 +0,0 @@
-defmodule ExUnit.CTFormatter do
- @moduledoc false
-
- use GenEvent
-
- import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5,
- format_test_case_failure: 5]
-
- def init(opts) do
- file = File.open! "exunit.log", [:append]
- # We do not print filter in log file as exclusion of test with tag
- # pending: true is always done
- config = %{
- file: file,
- seed: opts[:seed],
- trace: opts[:trace],
- colors: Keyword.put_new(opts[:colors], :enabled, false),
- width: 80,
- tests_counter: 0,
- failures_counter: 0,
- skipped_counter: 0,
- invalids_counter: 0
- }
- {:ok, config}
- end
-
- def handle_event({:suite_started, _opts}, config) do
- {:ok, config}
- end
-
- def handle_event({:suite_finished, run_us, load_us}, config) do
- print_suite(config, run_us, load_us)
- File.close config[:file]
- :remove_handler
- end
-
- def handle_event({:test_started, %ExUnit.Test{} = test}, config) do
- if config.tests_counter == 0, do: IO.binwrite config[:file], "== Running #{test.case} ==\n\n"
- {:ok, config}
- end
-
- def handle_event({:test_finished, %ExUnit.Test{state: nil} = _test}, config) do
- IO.binwrite config[:file], "."
- {:ok, %{config | tests_counter: config.tests_counter + 1}}
- end
-
- def handle_event({:test_finished, %ExUnit.Test{state: {:skip, _}} = _test}, config) do
- {:ok, %{config | tests_counter: config.tests_counter + 1,
- skipped_counter: config.skipped_counter + 1}}
- end
-
- def handle_event({:test_finished, %ExUnit.Test{state: {:invalid, _}} = _test}, config) do
- IO.binwrite config[:file], "?"
- {:ok, %{config | tests_counter: config.tests_counter + 1,
- invalids_counter: config.invalids_counter + 1}}
- end
-
- def handle_event({:test_finished, %ExUnit.Test{state: {:failed, failures}} = test}, config) do
- formatted = format_test_failure(test, failures, config.failures_counter + 1,
- config.width, &formatter(&1, &2, config))
- print_failure(formatted, config)
- print_logs(test.logs)
-
- {:ok, %{config | tests_counter: config.tests_counter + 1,
- failures_counter: config.failures_counter + 1}}
- end
-
- def handle_event({:case_started, %ExUnit.TestCase{}}, config) do
- {:ok, config}
- end
-
- def handle_event({:case_finished, %ExUnit.TestCase{state: nil}}, config) do
- {:ok, config}
- end
-
- def handle_event({:case_finished, %ExUnit.TestCase{state: {:failed, failures}} = test_case}, config) do
- formatted = format_test_case_failure(test_case, failures, config.failures_counter + 1,
- config.width, &formatter(&1, &2, config))
- print_failure(formatted, config)
- {:ok, %{config | failures_counter: config.failures_counter + 1}}
- end
-
- ## Printing
-
- defp print_suite(config, run_us, load_us) do
- IO.binwrite config[:file], "\n\n"
- IO.binwrite config[:file], format_time(run_us, load_us)
- IO.binwrite config[:file], "\n\n"
-
- # singular/plural
- test_pl = pluralize(config.tests_counter, "test", "tests")
- failure_pl = pluralize(config.failures_counter, "failure", "failures")
-
- message =
- "#{config.tests_counter} #{test_pl}, #{config.failures_counter} #{failure_pl}"
- |> if_true(config.skipped_counter > 0, & &1 <> ", #{config.skipped_counter} skipped")
- |> if_true(config.invalids_counter > 0, & &1 <> ", #{config.invalids_counter} invalid")
-
- cond do
- config.failures_counter > 0 -> IO.binwrite config[:file], message
- config.invalids_counter > 0 -> IO.binwrite config[:file], message
- true -> IO.binwrite config[:file], message
- end
-
- IO.binwrite config[:file], "\nRandomized with seed #{config.seed}\n\n\n\n"
- end
-
- defp if_true(value, false, _fun), do: value
- defp if_true(value, true, fun), do: fun.(value)
-
- defp print_failure(formatted, config) do
- IO.binwrite config[:file], "\n"
- IO.binwrite config[:file], formatted
- IO.binwrite config[:file], "\n"
- end
-
- defp formatter(_, msg, _config),
- do: msg
-
- defp pluralize(1, singular, _plural), do: singular
- defp pluralize(_, _singular, plural), do: plural
-
- defp print_logs(""), do: nil
-
- defp print_logs(output) do
- indent = "\n "
- output = String.replace(output, "\n", indent)
- IO.puts([" The following output was logged:", indent | output])
- end
-end
diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex
new file mode 100644
index 000000000..9d17b157d
--- /dev/null
+++ b/lib/ejabberd/config/attr.ex
@@ -0,0 +1,119 @@
+defmodule Ejabberd.Config.Attr do
+ @moduledoc """
+ Module used to work with the attributes parsed from
+ an elixir block (do...end).
+
+ Contains functions for extracting attrs from a block
+ and validation.
+ """
+
+ @type attr :: {atom(), any()}
+
+ @attr_supported [
+ active:
+ [type: :boolean, default: true],
+ git:
+ [type: :string, default: ""],
+ name:
+ [type: :string, default: ""],
+ opts:
+ [type: :list, default: []],
+ dependency:
+ [type: :list, default: []]
+ ]
+
+ @doc """
+ Takes a block with annotations and extracts the list
+ of attributes.
+ """
+ @spec extract_attrs_from_block_with_defaults(any()) :: [attr]
+ def extract_attrs_from_block_with_defaults(block) do
+ block
+ |> extract_attrs_from_block
+ |> put_into_list_if_not_already
+ |> insert_default_attrs_if_missing
+ end
+
+ @doc """
+ Takes an attribute or a list of attrs and validate them.
+
+ Returns a {:ok, attr} or {:error, attr, cause} for each of the attributes.
+ """
+ @spec validate([attr]) :: [{:ok, attr}] | [{:error, attr, atom()}]
+ def validate(attrs) when is_list(attrs), do: Enum.map(attrs, &valid_attr?/1)
+ def validate(attr), do: validate([attr]) |> List.first
+
+ @doc """
+ Returns the type of an attribute, given its name.
+ """
+ @spec get_type_for_attr(atom()) :: atom()
+ def get_type_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:type)
+ end
+
+ @doc """
+ Returns the default value for an attribute, given its name.
+ """
+ @spec get_default_for_attr(atom()) :: any()
+ def get_default_for_attr(attr_name) do
+ @attr_supported
+ |> Keyword.get(attr_name)
+ |> Keyword.get(:default)
+ end
+
+ # Private API
+
+ # Given an elixir block (do...end) returns a list with the annotations
+ # or a single annotation.
+ @spec extract_attrs_from_block(any()) :: [attr] | attr
+ defp extract_attrs_from_block({:__block__, [], attrs}), do: Enum.map(attrs, &extract_attrs_from_block/1)
+ defp extract_attrs_from_block({:@, _, [attrs]}), do: extract_attrs_from_block(attrs)
+ defp extract_attrs_from_block({attr_name, _, [value]}), do: {attr_name, value}
+ defp extract_attrs_from_block(nil), do: []
+
+ # In case extract_attrs_from_block returns a single attribute,
+ # then put it into a list. (Ensures attrs are always into a list).
+ @spec put_into_list_if_not_already([attr] | attr) :: [attr]
+ defp put_into_list_if_not_already(attrs) when is_list(attrs), do: attrs
+ defp put_into_list_if_not_already(attr), do: [attr]
+
+ # Given a list of attributes, it inserts the missing attribute with their
+ # default value.
+ @spec insert_default_attrs_if_missing([attr]) :: [attr]
+ defp insert_default_attrs_if_missing(attrs) do
+ Enum.reduce @attr_supported, attrs, fn({attr_name, _}, acc) ->
+ case Keyword.has_key?(acc, attr_name) do
+ true -> acc
+ false -> Keyword.put(acc, attr_name, get_default_for_attr(attr_name))
+ end
+ end
+ end
+
+ # Given an attribute, validates it and return a tuple with
+ # {:ok, attr} or {:error, attr, cause}
+ @spec valid_attr?(attr) :: {:ok, attr} | {:error, attr, atom()}
+ defp valid_attr?({attr_name, param} = attr) do
+ case Keyword.get(@attr_supported, attr_name) do
+ nil -> {:error, attr, :attr_not_supported}
+ [{:type, param_type} | _] -> case is_of_type?(param, param_type) do
+ true -> {:ok, attr}
+ false -> {:error, attr, :type_not_supported}
+ end
+ end
+ end
+
+ # Given an attribute value and a type, it returns a true
+ # if the value its of the type specified, false otherwise.
+
+ # Usefoul for checking if an attr value respects the type
+ # specified for the annotation.
+ @spec is_of_type?(any(), atom()) :: boolean()
+ defp is_of_type?(param, type) when type == :boolean and is_boolean(param), do: true
+ defp is_of_type?(param, type) when type == :string and is_bitstring(param), do: true
+ defp is_of_type?(param, type) when type == :list and is_list(param), do: true
+ defp is_of_type?(param, type) when type == :atom and is_atom(param), do: true
+ defp is_of_type?(_param, type) when type == :any, do: true
+ defp is_of_type?(_, _), do: false
+end
diff --git a/lib/ejabberd/config/config.ex b/lib/ejabberd/config/config.ex
new file mode 100644
index 000000000..a1b91858a
--- /dev/null
+++ b/lib/ejabberd/config/config.ex
@@ -0,0 +1,145 @@
+defmodule Ejabberd.Config do
+ @moduledoc """
+ Base module for configuration file.
+
+ Imports macros for the config DSL and contains functions
+ for working/starting the configuration parsed.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Attr
+ alias Ejabberd.Config.EjabberdLogger
+
+ defmacro __using__(_opts) do
+ quote do
+ import Ejabberd.Config, only: :macros
+ import Ejabberd.Logger
+
+ @before_compile Ejabberd.Config
+ end
+ end
+
+ # Validate the modules parsed and log validation errors at compile time.
+ # Could be also possible to interrupt the compilation&execution by throwing
+ # an exception if necessary.
+ def __before_compile__(_env) do
+ get_modules_parsed_in_order()
+ |> EjabberdModule.validate
+ |> EjabberdLogger.log_errors
+ end
+
+ @doc """
+ Given the path of the config file, it evaluates it.
+ """
+ def init(file_path, force \\ false) do
+ init_already_executed = Ejabberd.Config.Store.get(:module_name) != []
+
+ case force do
+ true ->
+ Ejabberd.Config.Store.stop
+ Ejabberd.Config.Store.start_link
+ do_init(file_path)
+ false ->
+ if not init_already_executed, do: do_init(file_path)
+ end
+ end
+
+ @doc """
+ Returns a list with all the opts, formatted for ejabberd.
+ """
+ def get_ejabberd_opts do
+ get_general_opts()
+ |> Map.put(:modules, get_modules_parsed_in_order())
+ |> Map.put(:listeners, get_listeners_parsed_in_order())
+ |> Ejabberd.Config.OptsFormatter.format_opts_for_ejabberd
+ end
+
+ @doc """
+ Register the hooks defined inside the elixir config file.
+ """
+ def start_hooks do
+ get_hooks_parsed_in_order()
+ |> Enum.each(&Ejabberd.Config.EjabberdHook.start/1)
+ end
+
+ ###
+ ### MACROS
+ ###
+
+ defmacro listen(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:listeners, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro module(module, do: block) do
+ attrs = Attr.extract_attrs_from_block_with_defaults(block)
+
+ quote do
+ Ejabberd.Config.Store.put(:modules, %EjabberdModule{
+ module: unquote(module),
+ attrs: unquote(attrs)
+ })
+ end
+ end
+
+ defmacro hook(hook_name, opts, fun) do
+ quote do
+ Ejabberd.Config.Store.put(:hooks, %Ejabberd.Config.EjabberdHook{
+ hook: unquote(hook_name),
+ opts: unquote(opts),
+ fun: unquote(fun)
+ })
+ end
+ end
+
+ # Private API
+
+ defp do_init(file_path) do
+ # File evaluation
+ Code.eval_file(file_path) |> extract_and_store_module_name()
+
+ # Getting start/0 config
+ Ejabberd.Config.Store.get(:module_name)
+ |> case do
+ nil -> IO.puts "[ ERR ] Configuration module not found."
+ [module] -> call_start_func_and_store_data(module)
+ end
+
+ # Fetching git modules and install them
+ get_modules_parsed_in_order()
+ |> EjabberdModule.fetch_git_repos
+ end
+
+ # Returns the modules from the store
+ defp get_modules_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:modules) |> Enum.reverse
+
+ # Returns the listeners from the store
+ defp get_listeners_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:listeners) |> Enum.reverse
+
+ defp get_hooks_parsed_in_order,
+ do: Ejabberd.Config.Store.get(:hooks) |> Enum.reverse
+
+ # Returns the general config options
+ defp get_general_opts,
+ do: Ejabberd.Config.Store.get(:general) |> List.first
+
+ # Gets the general ejabberd options calling
+ # the start/0 function and stores them.
+ defp call_start_func_and_store_data(module) do
+ opts = apply(module, :start, [])
+ Ejabberd.Config.Store.put(:general, opts)
+ end
+
+ # Stores the configuration module name
+ defp extract_and_store_module_name({{:module, mod, _bytes, _}, _}) do
+ Ejabberd.Config.Store.put(:module_name, mod)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_hook.ex b/lib/ejabberd/config/ejabberd_hook.ex
new file mode 100644
index 000000000..8b7858d23
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_hook.ex
@@ -0,0 +1,23 @@
+defmodule Ejabberd.Config.EjabberdHook do
+ @moduledoc """
+ Module containing functions for manipulating
+ ejabberd hooks.
+ """
+
+ defstruct hook: nil, opts: [], fun: nil
+
+ alias Ejabberd.Config.EjabberdHook
+
+ @type t :: %EjabberdHook{}
+
+ @doc """
+ Register a hook to ejabberd.
+ """
+ @spec start(EjabberdHook.t) :: none
+ def start(%EjabberdHook{hook: hook, opts: opts, fun: fun}) do
+ host = Keyword.get(opts, :host, :global)
+ priority = Keyword.get(opts, :priority, 50)
+
+ :ejabberd_hooks.add(hook, host, fun, priority)
+ end
+end
diff --git a/lib/ejabberd/config/ejabberd_module.ex b/lib/ejabberd/config/ejabberd_module.ex
new file mode 100644
index 000000000..6a74fe460
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_module.ex
@@ -0,0 +1,69 @@
+defmodule Ejabberd.Config.EjabberdModule do
+ @moduledoc """
+ Module representing a module block in the configuration file.
+ It offers functions for validation and for starting the modules.
+
+ Warning: The name is EjabberdModule to not collide with
+ the already existing Elixir.Module.
+ """
+
+ @type t :: %{module: atom, attrs: [Attr.t]}
+
+ defstruct [:module, :attrs]
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Validation
+
+ @doc """
+ Given a list of modules / single module
+ it runs different validators on them.
+
+ For each module, returns a {:ok, mod} or {:error, mod, errors}
+ """
+ def validate(modules) do
+ Validation.validate(modules)
+ end
+
+ @doc """
+ Given a list of modules, it takes only the ones with
+ a git attribute and tries to fetch the repo,
+ then, it install them through :ext_mod.install/1
+ """
+ @spec fetch_git_repos([EjabberdModule.t]) :: none()
+ def fetch_git_repos(modules) do
+ modules
+ |> Enum.filter(&is_git_module?/1)
+ |> Enum.each(&fetch_and_install_git_module/1)
+ end
+
+ # Private API
+
+ defp is_git_module?(%EjabberdModule{attrs: attrs}) do
+ case Keyword.get(attrs, :git) do
+ "" -> false
+ repo -> String.match?(repo, ~r/((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?/)
+ end
+ end
+
+ defp fetch_and_install_git_module(%EjabberdModule{attrs: attrs}) do
+ repo = Keyword.get(attrs, :git)
+ mod_name = case Keyword.get(attrs, :name) do
+ "" -> infer_mod_name_from_git_url(repo)
+ name -> name
+ end
+
+ path = "#{:ext_mod.modules_dir()}/sources/ejabberd-contrib\/#{mod_name}"
+ fetch_and_store_repo_source_if_not_exists(path, repo)
+ :ext_mod.install(mod_name) # Have to check if overwrites an already present mod
+ end
+
+ defp fetch_and_store_repo_source_if_not_exists(path, repo) do
+ unless File.exists?(path) do
+ IO.puts "[info] Fetching: #{repo}"
+ :os.cmd('git clone #{repo} #{path}')
+ end
+ end
+
+ defp infer_mod_name_from_git_url(repo),
+ do: String.split(repo, "/") |> List.last |> String.replace(".git", "")
+end
diff --git a/lib/ejabberd/config/logger/ejabberd_logger.ex b/lib/ejabberd/config/logger/ejabberd_logger.ex
new file mode 100644
index 000000000..90970ba73
--- /dev/null
+++ b/lib/ejabberd/config/logger/ejabberd_logger.ex
@@ -0,0 +1,32 @@
+defmodule Ejabberd.Config.EjabberdLogger do
+ @moduledoc """
+ Module used to log validation errors given validated modules
+ given validated modules.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Given a list of modules validated, in the form of {:ok, mod} or
+ {:error, mod, errors}, it logs to the user the errors found.
+ """
+ @spec log_errors([EjabberdModule.t]) :: [EjabberdModule.t]
+ def log_errors(modules_validated) when is_list(modules_validated) do
+ Enum.each modules_validated, &do_log_errors/1
+ modules_validated
+ end
+
+ defp do_log_errors({:ok, _mod}), do: nil
+ defp do_log_errors({:error, _mod, errors}), do: Enum.each errors, &do_log_errors/1
+ defp do_log_errors({:attribute, errors}), do: Enum.each errors, &log_attribute_error/1
+ defp do_log_errors({:dependency, errors}), do: Enum.each errors, &log_dependency_error/1
+
+ defp log_attribute_error({{attr_name, _val}, :attr_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} is not supported."
+
+ defp log_attribute_error({{attr_name, val}, :type_not_supported}), do:
+ IO.puts "[ WARN ] Annotation @#{attr_name} with value #{inspect val} is not supported (type mismatch)."
+
+ defp log_dependency_error({module, :not_found}), do:
+ IO.puts "[ WARN ] Module #{inspect module} was not found, but is required as a dependency."
+end
diff --git a/lib/ejabberd/config/opts_formatter.ex b/lib/ejabberd/config/opts_formatter.ex
new file mode 100644
index 000000000..b7010ddfe
--- /dev/null
+++ b/lib/ejabberd/config/opts_formatter.ex
@@ -0,0 +1,46 @@
+defmodule Ejabberd.Config.OptsFormatter do
+ @moduledoc """
+ Module for formatting options parsed into the format
+ ejabberd uses.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Takes a keyword list with keys corresponding to
+ the keys requested by the ejabberd config (ex: modules: mods)
+ and formats them to be correctly evaluated by ejabberd.
+
+ Look at how Config.get_ejabberd_opts/0 is constructed for
+ more informations.
+ """
+ @spec format_opts_for_ejabberd([{atom(), any()}]) :: list()
+ def format_opts_for_ejabberd(opts) do
+ opts
+ |> format_attrs_for_ejabberd
+ end
+
+ defp format_attrs_for_ejabberd(opts) when is_list(opts),
+ do: Enum.map opts, &format_attrs_for_ejabberd/1
+
+ defp format_attrs_for_ejabberd({:listeners, mods}),
+ do: {:listen, format_listeners_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({:modules, mods}),
+ do: {:modules, format_mods_for_ejabberd(mods)}
+
+ defp format_attrs_for_ejabberd({key, opts}) when is_atom(key),
+ do: {key, opts}
+
+ defp format_mods_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ {mod, attrs[:opts]}
+ end
+ end
+
+ defp format_listeners_for_ejabberd(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ Keyword.put(attrs[:opts], :module, mod)
+ end
+ end
+end
diff --git a/lib/ejabberd/config/store.ex b/lib/ejabberd/config/store.ex
new file mode 100644
index 000000000..72beea64c
--- /dev/null
+++ b/lib/ejabberd/config/store.ex
@@ -0,0 +1,55 @@
+defmodule Ejabberd.Config.Store do
+ @moduledoc """
+ Module used for storing the modules parsed from
+ the configuration file.
+
+ Example:
+ - Store.put(:modules, mod1)
+ - Store.put(:modules, mod2)
+
+ - Store.get(:modules) :: [mod1, mod2]
+
+ Be carefoul: when retrieving data you get them
+ in the order inserted into the store, which normally
+ is the reversed order of how the modules are specified
+ inside the configuration file. To resolve this just use
+ a Enum.reverse/1.
+ """
+
+ @name __MODULE__
+
+ def start_link do
+ Agent.start_link(fn -> %{} end, name: @name)
+ end
+
+ @doc """
+ Stores a value based on the key. If the key already exists,
+ then it inserts the new element, maintaining all the others.
+ It uses a list for this.
+ """
+ @spec put(atom, any) :: :ok
+ def put(key, val) do
+ Agent.update @name, &Map.update(&1, key, [val], fn coll ->
+ [val | coll]
+ end)
+ end
+
+ @doc """
+ Gets a value based on the key passed.
+ Returns always a list.
+ """
+ @spec get(atom) :: [any]
+ def get(key) do
+ Agent.get @name, &Map.get(&1, key, [])
+ end
+
+ @doc """
+ Stops the store.
+ It uses Agent.stop underneath, so be aware that exit
+ could be called.
+ """
+ @spec stop() :: :ok
+ def stop do
+ Agent.stop @name
+ end
+end
diff --git a/lib/ejabberd/config/validator/validation.ex b/lib/ejabberd/config/validator/validation.ex
new file mode 100644
index 000000000..af582676e
--- /dev/null
+++ b/lib/ejabberd/config/validator/validation.ex
@@ -0,0 +1,38 @@
+defmodule Ejabberd.Config.Validation do
+ @moduledoc """
+ Module used to validate a list of modules.
+ """
+
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ @type mod_validation_result :: {:ok, EjabberdModule.t} | {:error, EjabberdModule.t, map}
+
+ alias Ejabberd.Config.EjabberdModule
+ alias Ejabberd.Config.Validator
+
+ @doc """
+ Given a module or a list of modules it runs validators on them
+ and returns {:ok, mod} or {:error, mod, errors}, for each
+ of them.
+ """
+ @spec validate([EjabberdModule.t] | EjabberdModule.t) :: [mod_validation_result]
+ def validate(modules) when is_list(modules), do: Enum.map(modules, &do_validate(modules, &1))
+ def validate(module), do: validate([module])
+
+ # Private API
+
+ @spec do_validate([EjabberdModule.t], EjabberdModule.t) :: mod_validation_result
+ defp do_validate(modules, mod) do
+ {modules, mod, %{}}
+ |> Validator.Attrs.validate
+ |> Validator.Dependencies.validate
+ |> resolve_validation_result
+ end
+
+ @spec resolve_validation_result(mod_validation) :: mod_validation_result
+ defp resolve_validation_result({_modules, mod, errors}) do
+ case errors do
+ err when err == %{} -> {:ok, mod}
+ err -> {:error, mod, err}
+ end
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_attrs.ex b/lib/ejabberd/config/validator/validator_attrs.ex
new file mode 100644
index 000000000..6a85c068d
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_attrs.ex
@@ -0,0 +1,28 @@
+defmodule Ejabberd.Config.Validator.Attrs do
+ @moduledoc """
+ Validator module used to validate attributes.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+
+ import Ejabberd.Config.ValidatorUtility
+ alias Ejabberd.Config.Attr
+
+ @doc """
+ Given a module (with the form used for validation)
+ it runs Attr.validate/1 on each attribute and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ errors = Enum.reduce mod.attrs, errors, fn(attr, err) ->
+ case Attr.validate(attr) do
+ {:ok, _attr} -> err
+ {:error, attr, cause} -> put_error(err, :attribute, {attr, cause})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_dependencies.ex b/lib/ejabberd/config/validator/validator_dependencies.ex
new file mode 100644
index 000000000..d44c8a136
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_dependencies.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.Validator.Dependencies do
+ @moduledoc """
+ Validator module used to validate dependencies specified
+ with the @dependency annotation.
+ """
+
+ # TODO: Duplicated from validator.ex !!!
+ @type mod_validation :: {[EjabberdModule.t], EjabberdModule.t, map}
+ import Ejabberd.Config.ValidatorUtility
+
+ @doc """
+ Given a module (with the form used for validation)
+ it checks if the @dependency annotation is respected and
+ returns the validation tuple with the errors updated, if found.
+ """
+ @spec validate(mod_validation) :: mod_validation
+ def validate({modules, mod, errors}) do
+ module_names = extract_module_names(modules)
+ dependencies = mod.attrs[:dependency]
+
+ errors = Enum.reduce dependencies, errors, fn(req_module, err) ->
+ case req_module in module_names do
+ true -> err
+ false -> put_error(err, :dependency, {req_module, :not_found})
+ end
+ end
+
+ {modules, mod, errors}
+ end
+end
diff --git a/lib/ejabberd/config/validator/validator_utility.ex b/lib/ejabberd/config/validator/validator_utility.ex
new file mode 100644
index 000000000..17805f748
--- /dev/null
+++ b/lib/ejabberd/config/validator/validator_utility.ex
@@ -0,0 +1,30 @@
+defmodule Ejabberd.Config.ValidatorUtility do
+ @moduledoc """
+ Module used as a base validator for validation modules.
+ Imports utility functions for working with validation structures.
+ """
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @doc """
+ Inserts an error inside the errors collection, for the given key.
+ If the key doesn't exists then it creates an empty collection
+ and inserts the value passed.
+ """
+ @spec put_error(map, atom, any) :: map
+ def put_error(errors, key, val) do
+ Map.update errors, key, [val], fn coll ->
+ [val | coll]
+ end
+ end
+
+ @doc """
+ Given a list of modules it extracts and returns a list
+ of the module names (which are Elixir.Module).
+ """
+ @spec extract_module_names(EjabberdModule.t) :: [atom]
+ def extract_module_names(modules) when is_list(modules) do
+ modules
+ |> Enum.map(&Map.get(&1, :module))
+ end
+end
diff --git a/lib/ejabberd/config_util.ex b/lib/ejabberd/config_util.ex
new file mode 100644
index 000000000..6592104a2
--- /dev/null
+++ b/lib/ejabberd/config_util.ex
@@ -0,0 +1,18 @@
+defmodule Ejabberd.ConfigUtil do
+ @moduledoc """
+ Module containing utility functions for
+ the config file.
+ """
+
+ @doc """
+ Returns true when the config file is based on elixir.
+ """
+ @spec is_elixir_config(list) :: boolean
+ def is_elixir_config(filename) when is_list(filename) do
+ is_elixir_config(to_string(filename))
+ end
+
+ def is_elixir_config(filename) do
+ String.ends_with?(filename, "exs")
+ end
+end
diff --git a/lib/ejabberd/module.ex b/lib/ejabberd/module.ex
new file mode 100644
index 000000000..9fb3f040c
--- /dev/null
+++ b/lib/ejabberd/module.ex
@@ -0,0 +1,19 @@
+defmodule Ejabberd.Module do
+
+ defmacro __using__(opts) do
+ logger_enabled = Keyword.get(opts, :logger, true)
+
+ quote do
+ @behaviour :gen_mod
+ import Ejabberd.Module
+
+ unquote(if logger_enabled do
+ quote do: import Ejabberd.Logger
+ end)
+ end
+ end
+
+ # gen_mod callbacks
+ def depends(_host, _opts), do: []
+ def mod_opt_type(_), do: []
+end
diff --git a/lib/mix/tasks/deps.tree.ex b/lib/mix/tasks/deps.tree.ex
new file mode 100644
index 000000000..a3439c40b
--- /dev/null
+++ b/lib/mix/tasks/deps.tree.ex
@@ -0,0 +1,94 @@
+defmodule Mix.Tasks.Ejabberd.Deps.Tree do
+ use Mix.Task
+
+ alias Ejabberd.Config.EjabberdModule
+
+ @shortdoc "Lists all ejabberd modules and their dependencies"
+
+ @moduledoc """
+ Lists all ejabberd modules and their dependencies.
+
+ The project must have ejabberd as a dependency.
+ """
+
+ def run(_argv) do
+ # First we need to start manually the store to be available
+ # during the compilation of the config file.
+ Ejabberd.Config.Store.start_link
+ Ejabberd.Config.init(:ejabberd_config.path())
+
+ Mix.shell.info "ejabberd modules"
+
+ Ejabberd.Config.Store.get(:modules)
+ |> Enum.reverse # Because of how mods are stored inside the store
+ |> format_mods
+ |> Mix.shell.info
+ end
+
+ defp format_mods(mods) when is_list(mods) do
+ deps_tree = build_dependency_tree(mods)
+ mods_used_as_dependency = get_mods_used_as_dependency(deps_tree)
+
+ keep_only_mods_not_used_as_dep(deps_tree, mods_used_as_dependency)
+ |> format_mods_into_string
+ end
+
+ defp build_dependency_tree(mods) do
+ Enum.map mods, fn %EjabberdModule{module: mod, attrs: attrs} ->
+ deps = attrs[:dependency]
+ build_dependency_tree(mods, mod, deps)
+ end
+ end
+
+ defp build_dependency_tree(_mods, mod, []), do: %{module: mod, dependency: []}
+ defp build_dependency_tree(mods, mod, deps) when is_list(deps) do
+ dependencies = Enum.map deps, fn dep ->
+ dep_deps = get_dependencies_of_mod(mods, dep)
+ build_dependency_tree(mods, dep, dep_deps)
+ end
+
+ %{module: mod, dependency: dependencies}
+ end
+
+ defp get_mods_used_as_dependency(mods) when is_list(mods) do
+ Enum.reduce mods, [], fn(mod, acc) ->
+ case mod do
+ %{dependency: []} -> acc
+ %{dependency: deps} -> get_mod_names(deps) ++ acc
+ end
+ end
+ end
+
+ defp get_mod_names([]), do: []
+ defp get_mod_names(mods) when is_list(mods), do: Enum.map(mods, &get_mod_names/1) |> List.flatten
+ defp get_mod_names(%{module: mod, dependency: deps}), do: [mod | get_mod_names(deps)]
+
+ defp keep_only_mods_not_used_as_dep(mods, mods_used_as_dep) do
+ Enum.filter mods, fn %{module: mod} ->
+ not (mod in mods_used_as_dep)
+ end
+ end
+
+ defp get_dependencies_of_mod(deps, mod_name) do
+ Enum.find(deps, &(Map.get(&1, :module) == mod_name))
+ |> Map.get(:attrs)
+ |> Keyword.get(:dependency)
+ end
+
+ defp format_mods_into_string(mods), do: format_mods_into_string(mods, 0)
+ defp format_mods_into_string([], _indentation), do: ""
+ defp format_mods_into_string(mods, indentation) when is_list(mods) do
+ Enum.reduce mods, "", fn(mod, acc) ->
+ acc <> format_mods_into_string(mod, indentation)
+ end
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, 0) do
+ "\n├── #{mod}" <> format_mods_into_string(deps, 2)
+ end
+
+ defp format_mods_into_string(%{module: mod, dependency: deps}, indentation) do
+ spaces = Enum.reduce 0..indentation, "", fn(_, acc) -> " " <> acc end
+ "\n│#{spaces}└── #{mod}" <> format_mods_into_string(deps, indentation + 4)
+ end
+end
diff --git a/lib/mod_presence_demo.ex b/lib/mod_presence_demo.ex
index ba5abe90e..3ce512c3d 100644
--- a/lib/mod_presence_demo.ex
+++ b/lib/mod_presence_demo.ex
@@ -1,16 +1,15 @@
defmodule ModPresenceDemo do
- import Ejabberd.Logger # this allow using info, error, etc for logging
- @behaviour :gen_mod
+ use Ejabberd.Module
def start(host, _opts) do
info('Starting ejabberd module Presence Demo')
- Ejabberd.Hooks.add(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.add(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
def stop(host) do
info('Stopping ejabberd module Presence Demo')
- Ejabberd.Hooks.delete(:set_presence_hook, host, __ENV__.module, :on_presence, 50)
+ Ejabberd.Hooks.delete(:set_presence_hook, host, __MODULE__, :on_presence, 50)
:ok
end
@@ -19,8 +18,12 @@ defmodule ModPresenceDemo do
:none
end
- # gen_mod callbacks
- def depends(_host, _opts), do: []
- def mod_opt_type(_), do: []
+ def depends(_host, _opts) do
+ []
+ end
+ def mod_options(_host) do
+ []
+ end
+
end