camstuart

camstuart

Help with idiomatic Elixir style when dealing with logic flow

Hello,

I am somewhat new to Elixir, and finding that I am having difficulty grasping how I should handle logic for a series of sequences in an “operation”. For example, I have a phoenix post controller that I am using to onboard an organisation and user. So there are a few steps.

  1. Verify where the request came from (I know this is not perfect)
  2. Create an organisation if one does not already exist
  3. Create a user for this organisation if one does not already exist

In this example I am relying on “halt” to essentially return early. But I think I might be approaching the problem in Elixir like an imperative language. But in a regular function where such a mechanism does (no return statement in the language) I get a bit lost on how I should manage control flow. This seems it should be broken up somehow.

I also have ended up with a rather “nested” outcome, which perhaps could (or should?) be avoided with that cool pipeline operator, but I don’t know how I would handle the unhappy path nicely.

I would really appreciate some feedback, and any learning resources that help people like me who have been working in imperative languages so long that we have trouble breaking the habit! Thanks for reading!

def onboard(conn, params) do
    case verify_zendesk_origin(conn) do
      {:ok, _origin} ->
        user_params = params["user"]

        organisation_attrs = %{
          subdomain: params["subdomain"],
          name: params["name"],
          public_key: params["public_key"]
        }

        case Organisations.upsert(organisation_attrs) do
          {:ok, organisation} ->
            user_attrs = %{
              external_id: to_string(user_params["id"]),
              name: user_params["name"],
              role: user_params["role"],
              avatar_url: user_params["avatarUrl"],
              organisation_id: organisation.id
            }

            case ExternalAccounts.upsert_user(user_attrs) do
              {:ok, user} ->
                conn
                |> put_status(:ok)
                |> json(%{user_id: user.id})
                |> halt()

              {:error, changeset} ->
                IO.inspect(changeset.errors, label: "user upsert (onboard) changeset errors")

                conn
                |> put_status(:unprocessable_entity)
                |> json(%{
                  error: "invalid user data",
                  details: changeset_error_to_string(changeset)
                })
                |> halt()
            end

          {:error, changeset} ->
            IO.inspect(changeset.errors, label: "organisation upsert (onboard) changeset errors")

            conn
            |> put_status(:unprocessable_entity)
            |> json(%{
              error: "invalid organisation data",
              details: changeset_error_to_string(changeset)
            })
            |> halt()
        end

      {:error, _reason} ->
        conn
        |> put_status(:forbidden)
        |> json(%{error: "Invalid origin"})
        |> halt()
    end
  end

Marked As Solved

Lucassifoni

Lucassifoni

The logic could be extracted to an use-case module.
The origin verification could also be a plug.

At a very high and very verbose level :


plug YourAppWeb.Plugs, :verify_zendesk_origin when action in [:onboard, ...]
alias YourAppWeb.UserFlows.OnboardUser

def onboard(conn, params) do
    with {_, {:ok, organisation}} <- {:upsert_organisation, OnboardUser.upsert_organisation(conn.assigns.organisation_attrs)},
            {_, {:ok, user_attrs}} <- {:user_attrs, OnboardUser.user_attrs_from_params_and_org(params, organisation)}
            {_, {:ok, user}} <- {:upsert_external_user, OnboardUser.upsert_external_user(user_attrs)} do
         conn |> put_status(:ok) |> json(%{user_id: user.id})
   else
     {:upsert_organisation, {:error, e}} -> # handle this case
     {:user_attrs, {:error, e}} -> #handle this one
     {:upsert_external_user, {:error, e}} # handle this one
     _ -> # etc
    end
end

I’ve put this fictional OnboardUser module in YourAppWeb because the helper function user_attrs_from_params_and_org depends on the params arg, so it is linked to the transport.
You can of course refine that and have a pure non-web logic OnboardUser use-case while having other extracted utilities to properly construct the arguments it consumes from the request.

There also are a few different ways to tag error tuples or triples with with.
I like to do it at the call site to keep the logic free of this tagging.

The more non-web your logic is, the more testable it becomes :slight_smile:

Edit : use-case based modules are an opinionated choice and not the idiomatic choice.

Also Liked

dimitarvp

dimitarvp

I agree that it’s noisy, it just became my default because very often I had to consume APIs where I couldn’t influence the return values of the functions (hence your {:organisation_error, error_information} was often not possible to have). To me with plus tagging seemed like the better cope.

We had lengthy discussions on the forum in the past and I could see the perceived benefits of making smaller functions that call the 3rd party API and slightly reshape their returns (very often you would want to convert :error to {:error, :cannot_parse_url} for example) but I could not see the objectively demonstrable value of (1) adding extra functions, (2) reshaping the return values of the wrapped API just so we emulate an extra atom in a tuple in a with chain. :person_shrugging: It was just a preference that many people stated that they have but I was still left unconvinced because, again, it was just a preference.

I have to be honest here, the older I get the more I want LESS freedom for anyone in programming to just do stuff as they prefer – myself included, I made quite some impressive messes many times and really should have been slapped back to other techniques.

Hermanverschooten

Hermanverschooten

I would definitely go for a with in this case. Take a look at this anti-pattern, and use it as an example. Create functions that return either an :ok-tuple or a specific :error-tuple (or triplet) for that case.

stefanluptak

stefanluptak

I would probably do something like this.

Usually, there are few types of errors that your web layer will handle.

Something like:

  • {:error, :not_found}
  • {:error, changeset}
  • {:error, "Some error message as string"}

If your context functions always return these, you can have your error handling in the fallback controller and then just do those nice with {:ok, something} <- YourContext.some_fun(params) calls. And if there’s some exception to that, you can handle it in the else clause of the with statement.

Where Next?

Popular in Questions Top

myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
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
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
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
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New

Other popular topics Top

johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52673 488
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 43487 311
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement