Search code examples
elixirecto

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

Solution

  • 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.