diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 0000000..5d63270 --- /dev/null +++ b/.iex.exs @@ -0,0 +1 @@ +use RatError diff --git a/CHANGELOG.md b/CHANGELOG.md index 2156b6a..855505f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,10 @@ ## TODO - * define functions and error fields of first version (in README). - * write tests. - ## Schedule -### Sep.22.2017 +### Release `0.0.1` - * init project. + * Exports function `rat_error` (after calling `use RatError`). + * Exports `rat_error` configuration (see `config/*.exs` for detail). + * Implements module `RatError.Formatter` and `RatError.Structure`. diff --git a/README.md b/README.md index e51685b..dc468d2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -# RatError +# Rat Error Provides helper functions for error handling: * detailed error description. * default and configurable error fields. -* ... ## Installation @@ -15,3 +14,67 @@ def deps do ] end ``` + +## Configuration + +```elixir +config :rat_error, RatError.Structure, + # Node Name, the default value is 'error'. + # + # If node is 'err', the structure is, + # + # %{ + # err: + # %{ + # code: :invalid_argument, + # file: "/home/dummy/my_app/web/user_controller.ex", + # function: {:authenticate, 1}, + # line: 123, + # message: "wrong token!", + # module: Elixir.MyApp.Registration.UserController + # } + # } + # + # If node is nil or an empty string, the node is removed. The fields are + # exposed outside, and the below configuration 'prefix' could be set to + # distinguish with other caller's keys. + node: :error, + + # Field Prefix, the default value is nil (NO prefix). + # + # If node is nil and prefix is 'err_', the structure is, + # + # %{ + # err_code: :invalid_argument, + # err_file: "/home/dummy/my_app/web/user_controller.ex", + # err_function: {:authenticate, 1}, + # err_line: 123, + # err_message: "wrong token!", + # err_module: Elixir.MyApp.Registration.UserController + # } + # + prefix: nil, + + # Support Keys + keys: + [ + # Error code defined by caller, e.g. an atom :no_entry, an integer 9 or a + # string "unexpected". + :code, + + # Error file path. + :file, + + # Error function name. + :function, + + # Error file line. + :line, + + # Error message of string passed in by caller. + :message, + + # Error module. + :module + ] +``` diff --git a/config/config.exs b/config/config.exs index d9c2c7d..c9c59bb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,30 +1,3 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. use Mix.Config -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. - -# You can configure your application as: -# -# config :rat_error, key: :value -# -# and access this configuration in your application as: -# -# Application.get_env(:rat_error, :key) -# -# You can also configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" +import_config "#{Mix.env}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..801a86f --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,61 @@ +use Mix.Config + +config :rat_error, RatError.Structure, + # Node Name, the default value is 'error'. + # + # If node is 'err', the structure is, + # + # %{ + # err: + # %{ + # code: :invalid_argument, + # file: "/home/dummy/my_app/web/user_controller.ex", + # function: {:authenticate, 1}, + # line: 123, + # message: "wrong token!", + # module: Elixir.MyApp.Registration.UserController + # } + # } + # + # If node is nil or an empty string, the node is removed. The fields are + # exposed outside, and the below configuration 'prefix' could be set to + # distinguish with other caller's keys. + node: :error, + + # Field Prefix, the default value is nil (NO prefix). + # + # If node is nil and prefix is 'err_', the structure is, + # + # %{ + # err_code: :invalid_argument, + # err_file: "/home/dummy/my_app/web/user_controller.ex", + # err_function: {:authenticate, 1}, + # err_line: 123, + # err_message: "wrong token!", + # err_module: Elixir.MyApp.Registration.UserController + # } + # + prefix: nil, + + # Support Keys + keys: + [ + # Error code defined by caller, e.g. an atom :no_entry, an integer 9 or a + # string "unexpected". + :code, + + # Error file path. + :file, + + # Error function name. + :function, + + # Error file line. + :line, + + # Error message of string passed in by caller. + :message, + + # Error module. + :module + ] diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..50fdabb --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,6 @@ +use Mix.Config + +config :rat_error, RatError.Structure, + node: :error, + prefix: nil, + keys: :code diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..3428160 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,14 @@ +use Mix.Config + +config :rat_error, RatError.Structure, + node: :error, + prefix: nil, + keys: + [ + :code, + :file, + :function, + :line, + :message, + :module + ] diff --git a/lib/rat_error.ex b/lib/rat_error.ex index dfd3c21..278fb9b 100644 --- a/lib/rat_error.ex +++ b/lib/rat_error.ex @@ -1,18 +1,51 @@ defmodule RatError do - @moduledoc """ - Documentation for RatError. - """ + @moduledoc File.read!("README.md") + + alias RatError.{Formatter, Structure} + + @doc false + defmacro __using__(opts \\ []) do + quote(bind_quoted: [opts: opts], location: :keep) do + structure = + Structure.create_from_default_config + |> Structure.update(opts) + + @structure structure + + import RatError + end + end @doc """ - Hello world. + Retrieves a RAT error with the specified parameters. + + This function is the main API of the plugin. + + Parameters 'error_code' and 'error_message' correspond to the support keys + :code and :message (see 'config/*.exs' for detail). Both could be ignored by + using the default values if keys :code and :message are disabled. + + Parameter 'opts' corresponds the Keyword configuration of 'RatError.Structure' + (see 'config/*.exs' for detail). If this parameter is passed in, it merges + with the configuration of 'RatError.Structure'. ## Examples - iex> RatError.hello - :world + Parameter 'opts' merges with the configuration in 'config/test.exs'. + + iex> opts = [keys: [:code, :message]] + iex> rat_error(:wrong_password, "Wrong password!", opts) + %{error: %{code: :wrong_password, message: "Wrong password!"}} """ - def hello do - :world + defmacro rat_error(error_code \\ nil, error_message \\ "", opts \\ []) do + quote(bind_quoted: [error_code: error_code, + error_message: error_message, + opts: opts], + location: :keep) do + structure = Structure.update(@structure, opts) + + Formatter.format(structure, __ENV__, error_code, error_message) + end end end diff --git a/lib/rat_error/formatter.ex b/lib/rat_error/formatter.ex new file mode 100644 index 0000000..d5f4d13 --- /dev/null +++ b/lib/rat_error/formatter.ex @@ -0,0 +1,73 @@ +defmodule RatError.Formatter do + @moduledoc """ + Formats a RAT error. + + Formatter is used to retrieve the error Map result by formatting the + parameters (error code, message, environment variables and so on) with the + specified Structure (see 'config/*.exs' for detail). + """ + + alias RatError.Structure + + @env_keys [ + :file, + :function, + :line, + :module + ] + + @doc """ + Format a RAT error with the specified Structure. + + ## Examples + + iex> structure = %Structure{node: :err, keys: [:code, :message]} + iex> message = "Bad response!" + iex> Formatter.format(structure, __ENV__, :bad_response, message) + %{err: %{code: :bad_response, message: "Bad response!"}} + + iex> structure = %Structure{keys: [:code, :message]} + iex> message = "Out of memory!" + iex> Formatter.format(structure, __ENV__, :no_memory, message) + %{code: :no_memory, message: "Out of memory!"} + + """ + def format(%Structure{} = structure, + %Macro.Env{} = env, + error_code, + error_message) do + + params = + %{} + |> format_code(structure, error_code) + |> format_message(structure, error_message) + |> format_env_values(structure, env) + + if node = structure.node do + %{node => params} + else + params + end + end + + defp format_code(params, structure, value), + do: format_entry(params, structure, :code, value) + + defp format_entry(params, structure, key, value) when is_atom(key) do + if structure.keys |> List.wrap |> Enum.member?(key) do + new_key = String.to_atom(to_string(structure.prefix) <> to_string(key)) + + Map.put(params, new_key, value) + else + params + end + end + + defp format_env_values(params, structure, env) do + Enum.reduce(@env_keys, params, + &format_entry(&2, structure, &1, Map.get(env, &1))) + end + + defp format_message(params, structure, value), + do: format_entry(params, structure, :message, value) +end diff --git a/lib/rat_error/structure.ex b/lib/rat_error/structure.ex new file mode 100644 index 0000000..21073a5 --- /dev/null +++ b/lib/rat_error/structure.ex @@ -0,0 +1,156 @@ +defmodule RatError.Structure do + @moduledoc """ + Specifies the Map structure of a RAT error. + + This struct could be created from the specified options as below, + + [ + node: :error, + prefix: nil, + keys: [:code, :message] + ] + + References the 'RatError.Structure' configuration in 'config/*.exs' for + detail. + """ + + alias __MODULE__ + require Logger + + defstruct [:node, :prefix, :keys] + + @support_keys [ + # Error code defined by caller, e.g. an atom :no_entry, an integer 9 or a + # string "unexpected". + :code, + + # Error file path. + :file, + + # Error function name. + :function, + + # Error file line. + :line, + + # Error message of string passed in by caller. + :message, + + # Error module. + :module + ] + + @doc """ + Creates the struct from the default 'RatError.Structure' configuration. + + The default configuration is set in 'config/*.exs'. + + ## Examples + + References 'config/test.exs' for the test configuration. + + iex> Structure.create_from_default_config + %RatError.Structure + { + node: :error, + prefix: nil, + keys: + [ + :code, + :file, + :function, + :line, + :message, + :module + ] + } + + """ + def create_from_default_config do + :rat_error + |> Application.get_env(RatError.Structure) + |> create + end + + @doc """ + Creates the struct from the specified options. + + ## Examples + + iex> Structure.create(node: :err, keys: [:code, :message]) + %RatError.Structure + { + node: :err, + prefix: nil, + keys: [:code, :message] + } + + iex> Structure.create(prefix: :err, keys: [:code, :message]) + %RatError.Structure + { + node: nil, + prefix: :err, + keys: [:code, :message] + } + + iex> Structure.create(keys: :code) + %RatError.Structure + { + node: nil, + prefix: nil, + keys: [:code] + } + + """ + def create(opts) when is_list(opts) do + keys = filter_keys(opts[:keys]) + + %Structure{node: opts[:node], prefix: opts[:prefix], keys: keys} + end + + @doc """ + Updates the struct with the specified options. + + ## Examples + + iex> structure = %Structure{node: :err, keys: [:code]} + iex> Structure.update(structure, node: :error, prefix: :err, keys: :message) + %RatError.Structure + { + node: :error, + prefix: :err, + keys: [:message] + } + + """ + def update(%Structure{} = structure, opts) when is_list(opts) do + params = + Enum.reduce([:node, :prefix, :keys], %{}, + fn(k, acc) -> + case Keyword.fetch(opts, k) do + {:ok, v} -> Map.put(acc, k, v) + :error -> acc + end + end) + + params = + if keys = params[:keys] do + %{params | keys: filter_keys(keys)} + else + params + end + + Map.merge(structure, params) + end + + defp filter_keys(keys) do + keys = List.wrap(keys) + filtered_keys = keys -- (keys -- @support_keys) + + if is_nil(List.first(filtered_keys)) do + Logger.warn("there is no support keys - '#{inspect(keys)}'!") + end + + filtered_keys + end +end diff --git a/mix.exs b/mix.exs index 5d02f2e..3745e2e 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,7 @@ defmodule RatError.Mixfile do start_permanent: Mix.env == :prod, description: description(), package: package(), - deps: deps() + deps: deps(), name: "Rat Error", source_url: "https://github.com/silathdiir/rat_error" ] diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..23a1be9 --- /dev/null +++ b/mix.lock @@ -0,0 +1,4 @@ +%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, + "credo": {:hex, :credo, "0.8.6", "335f723772d35da499b5ebfdaf6b426bfb73590b6fcbc8908d476b75f8cbca3f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]}, + "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], []}, + "ex_doc": {:hex, :ex_doc, "0.16.4", "4bf6b82d4f0a643b500366ed7134896e8cccdbab4d1a7a35524951b25b1ec9f0", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}} diff --git a/test/rat_error/formatter_test.exs b/test/rat_error/formatter_test.exs new file mode 100644 index 0000000..edc8df9 --- /dev/null +++ b/test/rat_error/formatter_test.exs @@ -0,0 +1,5 @@ +defmodule RatError.FormatterTest do + alias RatError.{Formatter, Structure} + use ExUnit.Case + doctest Formatter +end diff --git a/test/rat_error/structure_test.exs b/test/rat_error/structure_test.exs new file mode 100644 index 0000000..426f10c --- /dev/null +++ b/test/rat_error/structure_test.exs @@ -0,0 +1,5 @@ +defmodule RatError.StructureTest do + alias RatError.Structure + use ExUnit.Case + doctest Structure +end diff --git a/test/rat_error_test.exs b/test/rat_error_test.exs index 72a7b24..53863ae 100644 --- a/test/rat_error_test.exs +++ b/test/rat_error_test.exs @@ -1,8 +1,49 @@ defmodule RatErrorTest do use ExUnit.Case + use RatError doctest RatError - test "greets the world" do - assert RatError.hello() == :world + defp rat_error_fun(invalid_arg, opts \\ []) do + message = "Invalid argument '#{inspect(invalid_arg)}'!" + + rat_error(:invalid_argument, message, opts) + end + + test "checks error with default configuration" do + expected_result = + %{ + error: + %{ + code: :invalid_argument, + file: "rat_error_test.exs", + function: {:rat_error_fun, 2}, + line: 9, + message: "Invalid argument 'nil'!", + module: RatErrorTest + } + } + + real_result = rat_error_fun(nil) + + # retrieves the last component of the path. + file = Path.basename(real_result.error.file) + error = %{real_result.error | file: file} + + real_result = %{real_result | error: error} + + assert expected_result == real_result + end + + test "checks error with custom configuration" do + expected_result = + %{ + err_code: :invalid_argument, + err_message: "Invalid argument 'nil'!" + } + + real_result = + rat_error_fun(nil, node: nil, prefix: :err_, keys: [:code, :message]) + + assert expected_result == real_result end end