Commandex v0.2.0 Commandex View Source

Defines a command struct.

Commandex structs are 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.

Example

%{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

Link to this section Summary

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.

Link to this section Types

Link to this type

command()

View Source
command() :: %atom(){
  data: map(),
  errors: map(),
  halted: boolean(),
  params: map(),
  pipelines: [atom() | {module(), atom()} | function()],
  success: boolean()
}

Link to this section Functions

Link to this macro

command(list)

View Source (macro)
command([{:do, any()}]) :: no_return()

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

Link to this macro

data(name)

View Source (macro)
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

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)

View Source (macro)
param(atom()) :: 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)
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 must be of arity three (command, params, data), but can be defined multiple ways:

pipline :create_user # The name of a function inside the command's module
pipeline &YourModule.create_user/3
pipeline {YourModule, :create_user} 
Link to this function

put_data(command, key, val)

View Source
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
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