I have a library that converts request params from a Plug app into Ecto queries for dynamic filtering of database rows. Baiscally I have some utils that generate appropriate form fields (which you put in your template), and a function that consumes the parameters in the request and generates the Ecto query.
The format of the parameters is something like this:
<input type="text" name="_search[column_name][operator]" value="contains"></input>
<input type="text" name="_search[column_name][value]" value="xxx"></input>
This is parsed by Plug into:
params = %{
"_search" => %{
"column_name" => %{
"operator" => "contains",
"value" => "x"
}
}
}
And compiled into the following Ecto query:
import Ecto.Query, only: [from: 2]
query = from c in MySchema, where: c.column_name == "%x%"
This is very simple to use and understand if on filter appear only once in the where
clause.
But suppose I have something like:
query = from c in MySchema, where: (c.number > 1 and c.number < 6)
I can’t use the format above, because a map can’t repeat a key. I don’t think there’s anything I can do to coerce Plug into inputs with the same name in a form into a list instead of a map (by the way, in my opinion, using a list of pairs instead of a map would have been a better choice), and I don’t want to customize my pipeline with a special plug just because of this. I’d like to be able to decode the params as returned by plug.
So I’ve thought of two possibilities be able to reuse a field name:
- Change the format to:
params = %{
"_search" => %{
"uuid-long-random-string-of-hex-digits" => %{
"column_name" => %{
"operator" => "contains",
"value" => "x"
}
},
"another-random-uuid" => %{
"column_name" => %{
"operator" => "contains",
"value" => "y"
}
}
# ...
}
}
I could then have in my template:
<%= input_widget(field: :column_name, operator: "greater_than") %>
<%= input_widget(field: :column_name, operator: "less_than") %>
which would generate:
<input type="text" name="_search[random-uuid][column_name][operator]" value="contains"></input>
<input type="text" name="_search[random-uuid][column_name][value]" value="xxx"></input>
<input type="text" name="_search[another-random-uuid][column_name][operator]" value="contains"></input>
<input type="text" name="_search[another-random-uuid][column_name][value]" value="xxx"></input>
UUIDs can be generated randomly and independently, each time I instantiate a widget, I’m sure (except for the vanishingly unlikely collision) that the fields won’t overwrite each other won’t repeat. The main problem here is that this makes my functions non-deterministic.
- Use the fact that I (and users) will be doing something like:
<%= form_for_field_filters resource, url, options, fn f -> %>
<%= input_widget(f, :field1) %>
<%= input_widget(f, :field2) %>
<%= input_widget(f, :field3) %>
<% end %>
in which all the calls to input_widget
happen in the same process. That way, I can use the process dictionary to keep a monotonic counter, which is initialized to zero by form_for_field
and incremented by input_widget
. That way, although input_widget
isn’t deterministic in isolation, the the template itself will be deterministic.
The Plug params would be:
params = %{
"_search" => %{
"0" => %{
"column_name" => %{
"operator" => "contains",
"value" => "x"
}
},
"1" => %{
"column_name" => %{
"operator" => "contains",
"value" => "y"
}
},
"2" => %{
"column_name" => %{
"operator" => "contains",
"value" => "z"
}
}
# ...
}
}
The format is just as easy to parse as the one above, and even has the advantage of preserving order (which I don’t really use, of course, but it’s interesting nonetheless). The disadvantage is that it’s just as non-deterministic as the one above and messes with the Process dictionary… It also breaks if for some unfathomable reason the user decides to generate the input widgets in different processes…
- Use the builder pattern in which I pipe the result of one widget into the other widget. The disadvantage is that it doesn’t play well with EEx templates. I could always generate the form with normal Elixir code, for example:
widgets =
input_widget(f, field1)
|> input_widget(f, field2)
|> input_widget(f, field3)
Of all of these, the one I think I should implement is option 2. Do you think this is a legitimate use of the Process dictionary? I mean, even the elixir formatter does this: https://github.com/elixir-lang/elixir/blob/425cebf10c24193f42187664dae6aaa8dbe4486f/lib/elixir/lib/code/formatter.ex#L202, in a place where it feels less justified (why don’t they pass the options around?).
Thanks in advance for all the input.