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/ejabberd/config | |
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/ejabberd/config')
-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 |
11 files changed, 618 insertions, 0 deletions
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..4d1270bc1 --- /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 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..4de9a302e --- /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 000000000..270fbfaa6 --- /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..2fe00361a --- /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 000000000..94117ab21 --- /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 |