I have a behaviour to abstract over parsing URL query parameters for various Phoenix endpoints. It looks like this:
defmodule Query do
@callback from_query_params(params :: %{optional(String.t()) => any()}) ::
{:ok, parsed :: struct} | {:error, reason :: atom}
end
And a simple implementation looks like this:
defmodule SearchQuery do
@moduledoc "Parses URL query params for search endpoint"
@behaviour Query
@enforce_keys [:search_term]
defstruct @enforce_keys
@typespec t :: %__MODULE__{search_term: String.t()}
@impl Query
def from_query_params(%{"query" => query}) when query != "" do
{:ok, %__MODULE__{search_term: query}}
end
def from_query_params(_), do: {:error, :missing_search_term}
end
What I'd really like to say here is:
- The implementing module should provide a struct (call it
t()
) - The success type on
from_query_params/1
should use that structt()
, not just any struct
I suspect there's no way within the Elixir typespec language to express this, but I'd be delighted to be proven wrong.
While it’s impossible to express this in typespec, one might partially cover the requirements with a bit of metaprogramming.
If you are ok with having its own
Query
behaviour per implementation to distinguish return types, it could be done withAnd instead of
@behaviour Query
, useuse QueryBuilder
. That way the nestedQuery
module will have a proper return type and the compiler callback will raise if the target module does not declare the struct.