I tend to use pattern matching in function heads. Here’s how I might approach it.
@order_by_fields ["inserted_at", "title"]
def build_query(query, "order_by", "", _conn), do: query
def build_query(query, "order_by", "desc_" <> field, _conn) when field in @order_by_fields do
Ecto.Query.order_by(query, [p], [desc: String.to_atom(field)])
end
def build_query(query, "order_by", "asc_" <> field, _conn) when field in @order_by_fields do
Ecto.Query.order_by(query, [p], [asc: String.to_atom(field)])
end
def build_query(query, "order_by", order_by, _conn) when order_by != "" do
order_by = case order_by do
"desc_" <> field -> [desc: String.to_existing_atom(field)]
"asc_" <> field -> [asc: String.to_existing_atom(field)]
_other -> [] # will be ignored by the ecto query builder (the final sql statement won't have an ORDER BY)
end
Ecto.Query.order_by(query, order_by)
end
I wouldn’t pass strings to a “context”, though. I would parse these strings at the boundary and turn them into more manageable data structures like {:order_by, :desc, :inserted_at}, which would be easier to handle inside the “context”.
On the boundary (maybe a controller):
valid_order_bys = [
{"desc_inserted_at", {:desc, :inserted_at}}, # these can be automatically generated as well
# etc ...
]
Enum.map(valid_order_bys, fn {valid_order_by_string, valid_order_by_tuple} ->
defp parse_order_by(unquote(valid_order_by_string)), do: unquote(valid_order_by_tuple)
end)
defp parse_order_by(invalid_order_by_string) do
# raise or log an error
end
# other parse_search_options clauses
defp parse_search_options([{"order_by", order_by} | rest], acc) do # I suspect it's for a search, but you can name it whatever you want
parse_search_options(rest, [{:order_by, parse_order_by(order_by)} | acc])
end
# other parse_search_options clauses
Then in the “context”
# other build_query clauses
defp build_query([{:order_by, {direction, field} = opts} | rest], acc_query) do
build_query(rest, Ecto.Query.order_by(acc_query, [opts]))
end
# other build_query clauses
Thank you all for all possible solutions provided, I simply love Elixir. Much can be done with little code. I suspect other languages like JAVA won’t allow similar shortcuts, power and flexibility.
I may be wrong here but is it not slightly dangerous to use String.to_atom on what I suspect is user passed strings. This opens up an attack vector for memory issues because atoms are not garbage collected. Again correct me if I am wrong.
Yes… You need to validate input in my code to avoid bad surprise
I like @blatyo pattern matching solution, as it includes sanity check, but the main point is to separate order_by in direction/order, so that You need only one Ecto.Query command.
The reason I didn’t use String.to_existing_atom/1 is that it requires some code to have executed to create that atom. There are some situations where the atom may not exist yet, but will eventually. I check the resulting strings with field in @order_by_fields, so that I know those functions could only ever create two new atoms from user input.
Right, but if you’re calling String.to_existing_atom/1 on user input you still want to check if it’s a valid value, otherwise you’ll get an exception instead of being able to fall back to a valid behavior.