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