Joe Eifert
Joe Eifert

Reputation: 1387

How to efficiently rename an attribute in an ecto changeset before it's being used

I have come up with the following solution which I call before the cast:

attrs = payload_fields_to_payload(attrs)

The issue was that the incoming attrs can be atom-based, but doesn't have to be. So is there any faster or cleaner way than what I'm doing here? I'm just renaming a key in the attributes. It seems to be lots of code for this task.

def payload_fields_to_payload(attrs) do
    attrs = cond do
      Map.has_key?(attrs, "payload_fields") ->
        Map.put(attrs, "payload", Map.get(attrs, "payload_fields"))
      Map.has_key?(attrs, :payload_fields) ->
        Map.put(attrs, :payload, Map.get(attrs, :payload_fields))
      true -> attrs
    end
    attrs
end

Upvotes: 0

Views: 1045

Answers (1)

7stud
7stud

Reputation: 48649

I'm just renaming a key in the attributes.

That's not what your code actually does--your code adds a new key to the map:

defmodule A do
  def go do

    attrs_list = [
      %{"payload_fields" => "hello", type: "ABC"},
      %{payload_fields: "goodbye", type: "XYZ"},
      %{abc: "dog", xyz: "cat"}
    ]

    Enum.map(attrs_list, fn attrs -> payload_fields_to_payload(attrs) end)

  end

  def payload_fields_to_payload(attrs) do
    cond do
      Map.has_key?(attrs, "payload_fields") ->
        Map.put(attrs, "payload", Map.get(attrs, "payload_fields"))
      Map.has_key?(attrs, :payload_fields) ->
        Map.put(attrs, :payload, Map.get(attrs, :payload_fields))
      true -> attrs
    end
  end

end

output:

iex(1)> A.go           |                         |
[                      V                         V
  %{:type => "ABC", "payload" => "hello", "payload_fields" => "hello"},
  %{payload: "goodbye", payload_fields: "goodbye", type: "XYZ"},
  %{abc: "dog", xyz: "cat"}
]

But, if the old key gets filtered out by cast(), then it's no big deal.

I would use pattern matching and multiple function clauses instead of using logic inside the function body to determine what to do. The following solution replaces the key payload_fields with the key payload:

defmodule A do

  def go do

    attrs_list = [
      %{"payload_fields" => "hello", type: "ABC"},
      %{payload_fields: "goodbye", type: "XYZ"},
      %{abc: "dog", xyz: "cat"}
    ]

    Enum.map(attrs_list, fn attrs -> convert_key(attrs) end)

  end

  def convert_key(%{"payload_fields" => value}=map) do  #string key
    map
    |> Map.delete("payload_fields")
    |> Map.put("payload", value)
  end
  def convert_key(%{payload_fields: value}=map) do  # atom key
    map
    |> Map.delete(:payload_fields)
    |> Map.put(:payload, value)
  end
  def convert_key(map), do: map

end

output:

iex(1)> A.go
[
  %{:type => "ABC", "payload" => "hello"},
  %{payload: "goodbye", type: "XYZ"},
  %{abc: "dog", xyz: "cat"}
]

If you really want to add a new key to the map--rather than rename the key--the code simplifies to:

  def convert_key(%{"payload_fields" => value}=map) do
    Map.put(map, "payload", value)
  end
  def convert_key(%{payload_fields: value}=map) do
    Map.put(map, :payload, value)
  end
  def convert_key(map), do: map

The issue was that the incoming attrs can be atom-based

The problem with allowing that is: what if an attr map has 14 million atom keys? Boom! Your app crashes. That same thing can happen with millions of attr maps that each contain only a few atom keys. That's the reason why Phoenix uses string keys in the params map for form data--doing that prevents an attacker from flooding the atom table by sending millions of requests with unique keys.

Upvotes: 3

Related Questions