A repository of tips and tricks (in both English and French) curated by Mirego’s engineering team.
  • elixir

Using for comprehension instead of Enum.reduce/4

I recently had to deal with camelCase-formated JSON while working on REST payloads. I am using an embedded schema and Ecto.Changeset to standardize the params map into an internal struct. However, I prefer to use the language snake_case convention for fields.

Make it work.

At first, I added the following three lines to the controller; naively converting keys to snake_case using Macro.underscore/1.

iex(1)> params = %{"email" => "john.doe@email.com", "firstName" => "John", "lastName" => "Doe"}
iex(2)> Enum.reduce(params, %{}, fn {key, value}, acc ->
...(2)>   Map.put(acc, Macro.underscore(key), value)
...(2)> end)
%{"email" => "john.doe@email.com", "first_name" => "John", "last_name" => "Doe"}

This pattern is so common, there had to be a better way… I knew Elixir has special forms of for to deal with Enumerable, but I’m not using for enough so I went back to the documentation:

Getting Started > Comprehensions

[…] However, the result of a comprehension can be inserted into different data structures by passing the :into option to the comprehension.
https://elixir-lang.org/getting-started/comprehensions.html#the-into-option

The previous code block could be expressed in a single line!

iex(3)> for {key, value} <- params, into: %{}, do: {Macro.underscore(key), value}
%{"email" => "john.doe@email.com", "first_name" => "John", "last_name" => "Doe"}

Make it clean!

It works, but only for map with a single level of keys: values! Let’s add recursion on values so we can apply the same logic recursively to nested maps. And extract our code to a Plug so it can be included in a router pipeline or directly at the controller level.

defmodule Foo.Plugs.SnakeCaseParams do
  def call(%{params: params} = conn, _opts) do
    %{conn | params: convert(params)}
  end

  defp convert(params) when is_map(params) do
    for {key, value} <- params, into: %{}, do: {Macro.underscore(key), convert(value)}
  end

  defp convert(params) when is_list(params) do
    for value <- params, do: convert(value)
  end

  defp convert(params), do: params
end

It’s as simple as that 🤓