Elixir Behaviour that returns the implementer's struct type

312 Views Asked by At

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 struct t(), 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.

1

There are 1 best solutions below

0
On

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 with

defmodule QueryBuilder do
  defmacro __using__(opts \\ []) do
    quote do
      impl = __MODULE__
      defmodule Query do
        @callback from_query_params(map()) :: {:ok, %unquote(impl){}}

        def __after_compile__(env, _bytecode),
          do: env.module.__struct__
      end

      @behaviour Query
      @after_compile Query
    end
  end
end

And instead of @behaviour Query, use use QueryBuilder. That way the nested Query module will have a proper return type and the compiler callback will raise if the target module does not declare the struct.