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
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 byCommandex.data/1
.errors
- Errors generated during the pipeline withCommandex.put_error/3
halted
- Whether or not the pipeline was halted.params
- Parameters given to the command, defined byCommandex.param/1
.pipelines
- A list of pipeline functions to execute, defined byCommandex.pipeline/1
.success
- Whether or not the command was successful. This is only set totrue
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
Defines a command struct with params, data, and pipelines.
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
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
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.
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
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