Commandex

Make Elixir actions a first-class data type.

CIVersionTotal DownloadsLicenseLast UpdatedDocumentation

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

Installation

Add commandex as a mix.exs dependency:

def deps do
[
{:commandex, "~> 0.5.0"}
]
end

Example Usage

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.

Running a command is easy:

%{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: _errors} ->
# I'm a lazy programmer that writes catch-all error handling
end

For even leaner implementations, you can run a command by passing the params directly into &run/1 without using &new/1:

%{email: "example@example.com", password: "asdf1234"}
|> RegisterUser.run()

Contributing

Testing

Unit tests can be run with mix test.

Formatting

This project uses Elixir's mix format and Prettier for formatting. Add hooks in your editor of choice to run it after a save. Be sure it respects this project's .formatter.exs.

Commits

Git commit subjects use the Karma style.

License

Copyright (c) 2020-2024 Codedge LLC (https://www.codedge.io/)

This library is MIT licensed. See the LICENSE for details.