Plug: Form parameters with the same name (or "is this a legitimate use of the Process dictionary?")

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:

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

  1. 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…

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

1 Like

Have you tried something like

<input type="text" name="_search[column_name][operator][]" value="contains"></input>
<input type="text" name="_search[column_name][value][]" value="xxx"></input>

Or whatever corresponds to foo[]=bar&foo[]=baz format. Then it would be decoded into %{foo: ["bar", "baz"]} by Plug.Conn.Query.

2 Likes

Thanks! I’ve totally missed that page in the docs. Maybe it’s discoverability is not the best, or maybe I was just distracted.

I wonder if I should edit the post title. The question comes from when I thought it was impossible to coerce Plug into giving me lists :laughing: