ExRunner

Elixir library that provides a macro which converts the modules into operations for encapsulating business logics. It uses Ecto Schema (embedded_schema) for defining input / output and Ecto Changeset for the validations. :)

Installation

It can be installed by adding ex_runner to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_runner, "~> 0.1.0"}
  ]
end

Usage

Let's take an example to understand how this library works.

Example - Write an operation which creates the user session by taking email and password.

Step 1 - Define a module and add use ExRunner.

defmodule CreateSession do
  use ExRunner
end

Step 2 - Define input and output for the operation. Input is what will be given as parameters to this operation and Output is what will get returned from the operation as a result.

defmodule CreateSession do
  use ExRunner

  input do
    field :email, :string
    field :password, :string
  end

  output do
    field :session_id, Ecto.UUID
  end
end

This is just Ecto Schema For complex schema definition, embeds_one and embeds_many can be used.

Step 3 - Define validate function which adds the required validations on the inputs passed to the operation.

defmodule CreateSession do
  use ExRunner

  input do
    field :email, :string
    field :password, :string
  end

  output do
    field :session_id, Ecto.UUID
  end

  defp validate(changeset) do
    changeset
    |> validate_required([:email, :password])
    |> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
    |> validate_length(:password, min: 4)
  end
end

Validate function takes changeset as argument, changes of which contains the input passed and schema is of type input defined in step 2.

For complex validations, read Ecto Changeset

Step 4- Define execute function which performs business logic with the inputs provided.

defmodule CreateSession do
  use ExRunner

  input do
    field :email, :string
    field :password, :string
  end

  output do
    field :session_id, Ecto.UUID
  end

  defp validate(changeset) do
    changeset
    |> validate_required([:email, :password])
    |> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)
    |> validate_length(:password, min: 4)
  end

  defp execute(changeset) do
    params = changeset.changes

    case (params.email == "test@test.com" and params.password == "test") do
      true -> %{session_id: "870df8e8-3107-4487-8316-81e089b8c2cf"}
      false -> add_error(changeset, :credentials, "are invalid")
    end
  end
end

Execute function takes changeset as argument. Inputs / parameters can be found in changeset.changes. To add an error, just call add_error of Ecto.Changeset.

In case of :ok, return the needed response of type output defined in step 4.

In case of :error, return Ecto.Changeset.

Step 5 - Try running the operation.

# run with valid credentials
> CreateSession.run(email: "test@test.com", password: "test")
{:ok, %{session_id: "870df8e8-3107-4487-8316-81e089b8c2cf"}}

# run! with valid credentials
> CreateSession.run!(email: "test@test.com", password: "test")
%{session_id: "870df8e8-3107-4487-8316-81e089b8c2cf"}

# run with invalid email format
> CreateSession.run(email: "test", password: "test")
{:error,
#Ecto.Changeset<
  action: nil,
  changes: %{email: "test", password: "testi"},
  errors: [email: {"has invalid format", [validation: :format]}],
  data: #CreateSession.Input<>,
  valid?: false
>}

# run! with invalid email format
> CreateSession.run(email: "test", password: "test")
# raises Ecto.InvalidChangesetError 

# run with invalid credentials
> CreateSession.run(email: "test@test.com", password: "test1")
{:error,
#Ecto.Changeset<
  action: nil,
  changes: %{email: "test@test.com", password: "testi"},
  errors: [credentials: {"are invalid", []}],
  data: #CreateSession.Input<>,
  valid?: false
>}

# run! with invalid credentials
> CreateSession.run!(email: "test@test.com", password: "test1")
# raises Ecto.InvalidChangesetError 

I recommend reading How does the library work internally ? to understand in detail.

How does the library work internally ?

run can be called either with a keyword list or a map.

1 - It first filters the input and permits only the ones defined in input schema. This is also true for embeds_one and embeds_many.

2 - It checks the input against the field types defined in input. If invalid, it returns {:error, changeset}

3 - It calls validate which has been defined in the module. If invalid, it returns {:error, changeset}

4 - It calls execute which has been defined in the module. If execute returns changeset, it returns {:error, changeset}. If execute returns other than changeset, it stores it as output.

5 - It filters the output and permits only the ones defined in output schema. This is also true for embeds_one and embeds_many.

6 - It checks the output against the field types defined in output. If invalid, it raises Ecto.InvalidChangesetError.

7 - It returns the output as map finally. {:ok, output}

In case of run!, if the returned tuple is of {:error}, it raises errors. if the returned tuple is of {:ok}, it returns output.