zorbash / opus

A framework for pluggable business logic components
MIT License
354 stars 21 forks source link

Feature: add `.matching/2` sugar #22

Closed sitch closed 4 years ago

sitch commented 4 years ago
defmodule Workflow do
  use Opus.Pipeline, strict: true

  # Here `strict: true` would require adherence to the type specs below
  @type strict_input :: map()
  @type strict_result ::
          :ok
          | {:ok, map}
          | {:error, any}
          | {:error, step :: atom, error :: any(), results_so_far :: map()}

  step :lookup_member 
  check :is_member_ready?

  matching %{member: %Member{status: "NO_GOOD"} = member} do
    tee(:send_email, with: fn -> Email.build(member, "not_ready.html") end)
  end

  matching %{member: %Member{status: "GOOD"} = member, payload: %{code: code}} do
    step :give_vip_status
    tee :expire_partner_code, with: fn -> Partner.expire_code(code) end

    matching %{member: %Member{status: "GREAT"} = member} do
      tee(:send_email, with: fn -> Email.build(member, "very_ready.html") end)
    end
  end

  defp lookup_member(%{member_id: id}) do
    {:ok, %{member: %Member{id: id}}}
  end

  defp give_vip_status(%{member: %Member{status: "Good"} = member}}) do
    {:ok, %{member | status: "Great"}}
  end
end

Explanation:

Thread a Kernel.match/1 call through children of the :do block's :if clauses. Which would have the benefit of avoiding the need to name the eval step

Design Concerns:

We would need a strict mode to ensure only patching the pipeline with maps from ok tuples

If there were some sort of strict mode, would it be better to use:

  1. An Ecto.Multi style pipeline where each step puts its key and {:ok, result} into the map
  2. An Exunit.Callbacks.setup/(1/2) style pipeline, which is just a folding map merge
  3. A Plug.Conn style pipeline struct with nested :assigns and :results maps

PS

Could also name this bind, bind_match, with_match or otherwise.

zorbash commented 4 years ago

While I can see that in certain scenarios such a syntax would be very convenient.

However the workflow of the example could be written as:

defmodule Workflow do
  use Opus.Pipeline, strict: true

  step :lookup_member
  check :is_member_ready?
  tee :send_email,                if: &match?(%{status: "NO_GOOD"}, &1)
  step :give_vip_status,          if: &match?(%{status: "GOOD"}, &1)
  tee :expire_partner_code,       if: &match?(%{status: "GOOD"}, &1)
  tee :send_email,                if: &match?(%{status: "GREAT"}, &1)

  # Omitted implementation of the stages
end

Which arguable is much easier to follow as there's no nesting of the conditionals.