hpopp

hpopp

Commandex - Make complex actions a first class data type

I’ve found myself implementing the command pattern in Elixir many times over across various projects, often with slightly different implementations. This library standardizes a lot of best practices I’ve found, with a foundation that can grow into a really powerful tool.

https://github.com/codedge-llc/commandex

What is the command pattern? Only one of the most awesome patterns ever. Imagine wrapping all of your relevant params, data and errors into a struct that can be easily piped. Related business logic can live together in a single module file, instead of dirtying up Phoenix controllers or Ecto models. This library closely resembles Plug and Ecto changesets but with one key difference-- because everything is a module struct, you can easily implement all kinds of protocols for different flows of business logic.

Example

Getting started is easy. A 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

  ...pipeline functions go here
end

The command/1 macro automatically defines new/1 and run/1 functions on the module, as well as a struct with the given attributes of the block. Params are what’s given to the new/1 function, and it can take either a keyword list or a string/atom key map. Data fields are things generated over the course of running a command, and can be set with put_data/3.

Pipeline functions take three arguments (command, params, data), and must return a command. Structuring it this way makes for super simple pattern matching:

  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, Pbkdf2.hash_pwd_salt(password))
  end

Pipeline functions are run in the order in which they are defined. And remember, like Plug, Commandex will continue running through the pipeline unless you call halt/1. This allows for intelligent error handling further down the pipe.

Calling a function outside of the module? That’s easy. The following three definitions are equivalent…

pipeline :hash_password
pipeline {RegisterUser, :hash_password}
pipeline &RegisterUser.hash_password/3

If a command is fully run without calling halt/1, it will have success: true marked on the struct. Usage might look like this:

%{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, error: %{password: :not_given}} ->
    # Respond with a 400 or something

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

Future Plans

There’s many different directions this project can take, but two that I have in mind: automatic validations/casting and saga rollbacks.

Because of the way attributes are defined, adding types would be easy:

command do
  param :email, :string
  data :user, User
end

This would allow intelligent casting of Phoenix params, as well as errors if put_data/3 did something you did not expect.

Sagas might be a bit more difficult, and while I might not strive for something as complex and powerful as Sage, rollbacks could be as easy as:

pipeline :create_user, rollback: :delete_user

Feedback

What kind of API would you like to see? Is the command macro straightforward? I’m open to ideas. I’ve begun converting many of my old custom command implementations to commandex, and it works really well for my use cases.

Most Liked

dimitarvp

dimitarvp

Few notes / questions, mostly around naming or convenience:

  • If I end up using such a library I’d appreciate it if I had a convenience function that does both new() and run(). Something like create_and_run(), maybe? Or make_and_run()? Or simply start?
  • Not sure how intuitive the naming of params and data is. I’d be a bit more formal and christen them input and output.
  • Ditto for pipeline. I get it that you probably want Plug users to feel at home but IMO something like action is clearer.
  • In my eyes {:ok, %{user: user, password_hash: "123"}} and {:error, error_object_or_message} are more idiomatic Elixir. (Note that Ecto.Multi partially follows your pattern but still uses tuples.)

To recap, this is how I’d find the whole thing more readable:

defmodule RegisterUser do
  import Commandex

  command do
    input :email
    input :password

    output :password_hash
    output :user

    action :hash_password
    action :create_user
    action :send_welcome_email
  end

  ...action functions go here
end

Don’t take my criticism seriously if you like your naming. I am only giving you a pure anecdote on what I’d find more intuitive / readable.


Lastly, as a friendly competition, you can take a look at Opus (they seem to like your pipeline naming :wink: ).

gregvaughn

gregvaughn

The very fact that there are different, well-reasoned, opinions on which direction to take things has kept me skeptical if building a library is worthwhile, vs. educational materials on the pattern itself.

gregvaughn

gregvaughn

There’s value in a library, but there’s always tradeoffs involved. I’m probably not thinking creatively enough, but I’m not seeing a lot of value for protocols for this, so the value in keeping that an option is lost on me.

I’ve been building out a custom command pattern use in my primary work system. I have it set up to return {:ok, command_struct} or {:error, exception_struct} and I’m liking the affordances that offers.

Perhaps there’s some middle ground to be had if the library offered both bang and non-bang new and run functions. Protocols only work with the bang functions, but for something more bespoke, you can give that up and use the non-bang versions. OTOH, that might tip the library into the realm of too complex ¯_(ツ)_/¯

Where Next?

Popular in Announcing Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
tmbb
I’ve published the first version of my Makeup library. It’s a syntax highlighter for Elixir in the spirit of Pygments, Currently it highl...
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
gabrielpoca
Hello everyone! I want to share with you something that I’m really proud of: https://stillstatic.io/ Still is a static site builder for...
New
devonestes
Introducing assertions, the library that helps you write really great test assertions! GitHub: GitHub - devonestes/assertions: Helpful a...
New
cjen07
parameterized pipe in elixir: |n> edit: negative index in |n> and mixed usage with |> are supported example: use ParamPipe ...
New
aditya7iyengar
Rummage.Ecto and Rummage.Phoenix provide ways to perform Searching, Sorting and Pagination over Ecto queries and Phoenix collections. Fo...
New
OvermindDL1
Been making an MLElixir thing (not released yet…) for fun in spare time in the past day. I’m just trying to see how much I can get an ML...
132 13966 106
New
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43622 214
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New

We're in Beta

About us Mission Statement