View Source Commandex (Commandex v0.5.0)

Defines a command struct.

Commandex is a loose implementation of the command pattern, making it easy to wrap parameters, data, and errors into a well-defined struct.

Example

A fully implemented command module might look like this:

defmodule RegisterUser do
  import Commandex

  command do
    param :email
    param :password

    data :password_hash
    data :user

    pipeline :hash_password
    pipeline :create_user
    pipeline :send_welcome_email
  end

  def hash_password(command, %{password: nil} = _params, _data) do
    command
    |> put_error(:password, :not_given)
    |> halt()
  end

  def hash_password(command, %{password: password} = _params, _data) do
    put_data(command, :password_hash, Base.encode64(password))
  end

  def create_user(command, %{email: email} = _params, %{password_hash: phash} = _data) do
    %User{}
    |> User.changeset(%{email: email, password_hash: phash})
    |> Repo.insert()
    |> case do
      {:ok, user} -> put_data(command, :user, user)
      {:error, changeset} -> command |> put_error(:repo, changeset) |> halt()
    end
  end

  def send_welcome_email(command, _params, %{user: user}) do
    Mailer.send_welcome_email(user)
    command
  end
end

The command/1 macro will define a struct that looks like:

%RegisterUser{
  success: false,
  halted: false,
  errors: %{},
  params: %{email: nil, password: nil},
  data: %{password_hash: nil, user: nil},
  pipelines: [:hash_password, :create_user, :send_welcome_email]
}

As well as two functions:

&RegisterUser.new/1
&RegisterUser.run/1

&new/1 parses parameters into a new struct. These can be either a keyword list or map with atom/string keys.

&run/1 takes a command struct and runs it through the pipeline functions defined in the command. Functions are executed in the order in which they are defined. If a command passes through all pipelines without calling halt/1, :success will be set to true. Otherwise, subsequent pipelines after the halt/1 will be ignored and :success will be set to false.

%{email: "example@example.com", password: "asdf1234"}
|> RegisterUser.new()
|> RegisterUser.run()
|> case do
  %{success: true, data: %{user: user}} ->
    # Success! We've got a user now

  %{success: false, errors: %{password: :not_given}} ->
    # Respond with a 400 or something

  %{success: false, errors: _error} ->
    # I'm a lazy programmer that writes catch-all error handling
end

Parameter-less Commands

If a command does not have any parameters defined, a run/0 will be generated automatically. Useful for diagnostic jobs and internal tasks.

iex> GenerateReport.run()
%GenerateReport{
  pipelines: [:fetch_data, :calculate_results],
  data: %{total_valid: 183220, total_invalid: 781215},
  params: %{},
  halted: false,
  errors: %{},
  success: true
}

Summary

Types

Command struct.

Command pipeline stage.

Functions

Defines a command struct with params, data, and pipelines.

Defines a command data field.

Halts a command pipeline.

Defines a command parameter field.

Defines a command pipeline.

Sets a data field with given value.

Sets error for given key and value.

Types

@type command() :: %{
  __struct__: atom(),
  data: map(),
  errors: map(),
  halted: boolean(),
  params: map(),
  pipelines: [pipeline()],
  success: boolean()
}

Command struct.

Attributes

  • data - Data generated during the pipeline, defined by Commandex.data/1.
  • errors - Errors generated during the pipeline with Commandex.put_error/3
  • halted - Whether or not the pipeline was halted.
  • params - Parameters given to the command, defined by Commandex.param/1.
  • pipelines - A list of pipeline functions to execute, defined by Commandex.pipeline/1.
  • success - Whether or not the command was successful. This is only set to true if the command was not halted after running all of the pipelines.
@type pipeline() ::
  atom()
  | {module(), atom()}
  | {module(), atom(), [any()]}
  | (command :: struct() -> command :: struct())
  | (command :: struct(), params :: map(), data :: map() -> command :: struct())

Command pipeline stage.

A pipeline function can be defined multiple ways:

  • pipeline :do_work - Name of a function inside the command's module, arity three.
  • pipeline {YourModule, :do_work} - Arity three.
  • pipeline {YourModule, :do_work, [:additonal, "args"]} - Arity three plus the number of additional args given.
  • pipeline &YourModule.do_work/1 - Or any anonymous function of arity one.
  • pipeline &YourModule.do_work/3 - Or any anonymous function of arity three.

Functions

@spec command([{:do, any()}]) :: no_return()

Defines a command struct with params, data, and pipelines.

@spec data(atom()) :: no_return()

Defines a command data field.

Data field values are created and set as pipelines are run. Set one with put_data/3.

command do
  # ...params

  data :password_hash
  data :user

  # ...pipelines
end
@spec halt(command()) :: command()

Halts a command pipeline.

Any pipelines defined after the halt will be ignored. If a command finishes running through all pipelines, :success will be set to true.

def hash_password(command, %{password: nil} = _params, _data) do
  command
  |> put_error(:password, :not_supplied)
  |> halt()
end
Link to this macro

param(name, opts \\ [])

View Source (macro)
@spec param(atom(), Keyword.t()) :: no_return()

Defines a command parameter field.

Parameters are supplied at struct creation, before any pipelines are run.

command do
  param :email
  param :password

  # ...data
  # ...pipelines
end
Link to this macro

pipeline(name)

View Source (macro)
@spec pipeline(atom()) :: no_return()

Defines a command pipeline.

Pipelines are functions executed against the command, in the order in which they are defined.

For example, two pipelines could be defined:

pipeline :check_valid_email
pipeline :create_user

Which could be mentally interpreted as:

command
|> check_valid_email()
|> create_user()

A pipeline function can be defined multiple ways:

  • pipeline :do_work - Name of a function inside the command's module, arity three.
  • pipeline {YourModule, :do_work} - Arity three.
  • pipeline {YourModule, :do_work, [:additonal, "args"]} - Arity three plus the number of additional args given.
  • pipeline &YourModule.do_work/1 - Or any anonymous function of arity one.
  • pipeline &YourModule.do_work/3 - Or any anonymous function of arity three.
Link to this function

put_data(command, key, val)

View Source
@spec put_data(command(), atom(), any()) :: command()

Sets a data field with given value.

Define a data field first:

data :password_hash

Set the password pash in one of your pipeline functions:

def hash_password(command, %{password: password} = _params, _data) do
  # Better than plaintext, I guess
  put_data(command, :password_hash, Base.encode64(password))
end
Link to this function

put_error(command, key, val)

View Source
@spec put_error(command(), any(), any()) :: command()

Sets error for given key and value.

:errors is a map. Putting an error on the same key will overwrite the previous value.

def hash_password(command, %{password: nil} = _params, _data) do
  command
  |> put_error(:password, :not_supplied)
  |> halt()
end