How to ensure atom exists?

I have in module a very long list of filters like this:

@mapping %{
    published_at: {:date_time, "published_at"},
    indexed_at: {:date_time, "indexed_at"},
    ...
    not_parent_shared_uid: {:terms_exclusion, "parent.shared_uid"},
    ...
    }

Because I wan’t to dynamically call some functions like this

  def parse_filter({filter_name, value}, topic_id, acc) do
    {function, field} = Map.get(@mapping, String.to_existing_atom(filter_name))
    apply(__MODULE__, function, [value, field, topic_id, acc])
  end

Yet for existing atoms in @mapping I sometimes, occasionally get

Elixir.ArgumentError in :erlang.binary_to_existing_atom/2
arg0: "not_parent_shared_uid"
arg1: :utf8

So the question is, how can I make sure that those atoms do exist?

I think this will solve your problem.
Just checking whether the filter_name is atom or not.

    {function, field} = 
       Map.get(@mapping, is_atom(filter_name) && filter_name || String.to_atom(filter_name))

Actually I am 100% sure the filter_name is binary (string actually), but it comes from a json, so I don’t want to use String.to_atom() to circumvent potential creation of invalid atoms. I have no problems with errors being raised when there is no atom in system, but the error is raised for valid atoms.

Also UPDATE: I copied modified code from my debug session earlier. Instead of to_atom I am actually using to_existing_atom. With to_atom it works as intended, the problem is with to_existing_atom :confused:

Maybe you can replace your map with a “function lookup”. And have mappings keys be strings? Would it change anything?

defmodule SomeModule do
  @moduledoc ""
  
  mappings = %{
    "published_at" => {:date_time, "published_at"},
    "indexed_at" => {:date_time, "indexed_at"},
    "not_parent_shared_uid" => {:terms_exclusion, "parent.shared_uid"}
  }

  @spec lookup_mapping(String.t()) :: {atom, String.t()} | nil
  defp lookup_mapping(filter_name)

  Enum.map(mappings, fn {filter_name, value} ->
    defp lookup_mapping(unquote(filter_name)) do
      unquote(value) # would create :date_time and :terms_exclusion atoms during compilation
    end
  end)

  defp lookup_mapping(_unmatched), do: nil

  def parse_filter({filter_name, value}, topic_id, acc) do
    case lookup_mapping(filter_name) do
      {function, field} ->
        apply(__MODULE__, function, [value, field, topic_id, acc])
      nil -> 
        acc
    end
  end
end
1 Like

Are you sure, that the string does not contain any unprintable bytes (zero width space or similar)?

But instead of relying on String.to_existing_atom/1, I’d use something like this:

@mapping %{...}

@filter_name_to_atom @mapping |> Enum.map(fn {atom, _} -> {to_string(atom), atom} end) |> Enum.into(%{})

  def parse_filter({filter_name, value}, topic_id, acc) do
    {function, field} = Map.get(@mapping, @filter_name_to_atom[filter_name])
    apply(__MODULE__, function, [value, field, topic_id, acc])
  end

Or if you need the crash on invalid filter names use Map.fetch!/2 instead of Access-Syntax.


edit

Using Map.fetch!/2 had actually the benefit that we would also fail for "ok", while String.to_existing_atom("ok") fould simply return :ok.

1 Like

@idi527: I’d rather use atoms as map keys, although it’s just a personal preference. But I’ll look more into your code, thanks :slight_smile:

I can’t be sure, it comes from the outside. It’s not really something I can control, I have to have “good faith” in the maintainer of the app that’s queering this app of mine. He’s got no malicious intent, though, as he’s a my work colleague.

With Map.fetch! this would fail every time exactly where it should, so I may go with this instead. Thanks :slight_smile:

1 Like