diff options
author | gabrielgatu <gabriel.dny@gmail.com> | 2016-09-08 11:34:42 +0200 |
---|---|---|
committer | Mickael Remond <mremond@process-one.net> | 2016-09-08 11:37:14 +0200 |
commit | 803270fc6b8ed3ba718f7e231b149caef70aa1ae (patch) | |
tree | cc4508758cbcec7a74568834888f3208d876a953 /lib | |
parent | Support 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.ex | 119 | ||||
-rw-r--r-- | lib/ejabberd/config/config.ex | 145 | ||||
-rw-r--r-- | lib/ejabberd/config/ejabberd_hook.ex | 23 | ||||
-rw-r--r-- | lib/ejabberd/config/ejabberd_module.ex | 70 | ||||
-rw-r--r-- | lib/ejabberd/config/logger/ejabberd_logger.ex | 32 | ||||
-rw-r--r-- | lib/ejabberd/config/opts_formatter.ex | 46 | ||||
-rw-r--r-- | lib/ejabberd/config/store.ex | 55 | ||||
-rw-r--r-- | lib/ejabberd/config/validator/validation.ex | 40 | ||||
-rw-r--r-- | lib/ejabberd/config/validator/validator_attrs.ex | 28 | ||||
-rw-r--r-- | lib/ejabberd/config/validator/validator_dependencies.ex | 30 | ||||
-rw-r--r-- | lib/ejabberd/config/validator/validator_utility.ex | 30 | ||||
-rw-r--r-- | lib/ejabberd/config_util.ex | 18 | ||||
-rw-r--r-- | lib/ejabberd/module.ex | 19 | ||||
-rw-r--r-- | lib/mix/tasks/deps.tree.ex | 94 | ||||
-rw-r--r-- | lib/mod_presence_demo.ex | 12 |
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 |