use with to deal with `Map.fetch/2`

I’m trying to parse a response of a json api by using with. The problems is Map.fetch/2 returens {:ok, any} | :error. And with requires {:ok, any} | {:error, any}. So I have to indent my code with multiple case level like following:

  def domain_info(url) do
    with {:ok, result} <- get("/v1/domain/info", query: [domain: url]) do
      case Map.fetch(result, :body) do
        {:ok, body} ->
          case Map.fetch(body, "code") do
            {:ok, code} ->
              case code do
                100_000 ->
                  {:ok,
                   Map.take(body, ["domain", "cname", "status"])
                   |> Map.update("status", "configuraing", fn cur ->
                     case cur do
                       4 -> "online"
                       6 -> "offline"
                       _ -> "configuring"
                     end
                   end)}
                _ ->
                  {:error, Map.take(body, ["code", "message"])}
              end
            :error ->
              {:error, "no key 'code' in body"}
          end
        :error ->
          {:error, "no key 'body' in response"}
      end
    end
  end

Is there a better way to do that? Our should I create a wrapper for Map.fetch which returns {:error, nil} when no such key found.

That’s not correct. with works with any value, and your supplied pattern(s) has to decide if to continue on the “success” side or go to the error block or return if there’s no error block.

The problem you encountered is that with doesn’t necessarily let you determine which pattern failed and that’s by design. with is meant for consolidating error cases on successive checks. If the error cases don’t provide the detail you need you’d want to add those details outside of the with.

For your example I’d probably go with something like this:

def domain_info(url) do
    with {:ok, result} <- get("/v1/domain/info", query: [domain: url]),
         {:ok, body} <- fetch_key(result, :body, "no key :body in response"),
         {:ok, code} <- fetch_key(body, "code", "no key \"code\" in body"),
         :ok <- check_code(code, Map.take(body, ["code", "message"])) do
      result =
        body
        |> Map.take(["domain", "cname", "status"])
        |> Map.update("status", "configuring", fn
          4 -> "online"
          6 -> "offline"
          _ -> "configuring"
        end)

      {:ok, result}
    end
  end

  defp fetch_key(map, key, error_message) do
    with :error <- Map.fetch(map, key) do
      {:error, error_message}
    end
  end

  defp check_code(100_000, _error), do: :ok
  defp check_code(_, error), do: {:error, error}
1 Like

thanks @LostKobrakai The fetch_key seems amazing! I’d use it. Thank you!