summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorgabrielgatu <gabriel.dny@gmail.com>2016-09-08 11:34:42 +0200
committerMickael Remond <mremond@process-one.net>2016-09-08 11:37:14 +0200
commit803270fc6b8ed3ba718f7e231b149caef70aa1ae (patch)
treecc4508758cbcec7a74568834888f3208d876a953 /lib
parentSupport for publishing to hex.pm with latest Elixir mix (diff)
Support for Elixir configuration file #1208
Contribution for Google Summer of code 2016 by Gabriel Gatu
Diffstat (limited to 'lib')
-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.ex70
-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.ex40
-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.ex12
15 files changed, 752 insertions, 9 deletions
diff --git a/lib/ejabberd/config/attr.ex b/lib/ejabberd/config/attr.ex
new file mode 100644
index 00000000..9d17b157
--- /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 00000000..4d1270bc
--- /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
+ |> Dict.put(:modules, get_modules_parsed_in_order())
+ |> Dict.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 00000000..8b7858d2
--- /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 00000000..4de9a302
--- /dev/null
+++ b/lib/ejabberd/config/ejabberd_module.ex
@@ -0,0 +1,70 @@
+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.Attr
+ 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 00000000..270fbfaa
--- /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 00000000..b7010ddf
--- /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 00000000..72beea64
--- /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 00000000..2fe00361
--- /dev/null
+++ b/lib/ejabberd/config/validator/validation.ex
@@ -0,0 +1,40 @@
+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.Attr
+ alias Ejabberd.Config.Validator
+ alias Ejabberd.Config.ValidatorUtility
+
+ @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 00000000..94117ab2
--- /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 00000000..d44c8a13
--- /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 00000000..17805f74
--- /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 00000000..6592104a
--- /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 00000000..9fb3f040
--- /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 00000000..94cb85a5
--- /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.get_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 ba5abe90..09bf5840 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
@@ -18,9 +17,4 @@ defmodule ModPresenceDemo do
info('Receive presence for #{user}')
:none
end
-
- # gen_mod callbacks
- def depends(_host, _opts), do: []
- def mod_opt_type(_), do: []
-
end