lib/pg_ranges.ex

defmodule PgRanges do
  @moduledoc """
  PgRanges provides a simple wrapper around `Postgrex.Range` so that you can
  create schemas with range type fields and use the native range type in
  migrations.

      defmodule MyApp.Employee do
        use Ecto.Schema
        alias PgRanges.DateRange

        schema "employees" do
          field :name, :string
          field :employed_dates, DateRange
        end
      end

      defmodule MyApp.Repo.Migrations.CreateEmployees do
        use Ecto.Migration

        def change do
          create table(:employees) do
            add :name, :string
            add :employed_dates, :daterange
          end
        end
      end

  When used in composing queries, you must cast to the PgRange type.

      import Ecto.Query
      alias PgRanges.DateRange
      alias MyApp.Repo

      range = DateRange.new(~D[2018-11-01], ~D[2019-01-01])

      Repo.all(
        from e in Employee,
        where: fragment("? @> ?", e.employed_dates, type(^range, DateRange))
      )
  """

  @callback new(lower :: any, upper :: any) :: any
  @callback new(any, any, any) :: any
  @callback from_postgrex(any) :: any
  @callback to_postgrex(any) :: any

  @optional_callbacks new: 2,
                      new: 3,
                      from_postgrex: 1,
                      to_postgrex: 1

  @doc false
  defmacro __using__(opts) do
    quote location: :keep, bind_quoted: [opts: opts] do
      alias Postgrex.Range

      use Ecto.Type
      @behaviour PgRanges
      @before_compile PgRanges

      defstruct lower: nil,
                lower_inclusive: true,
                upper: nil,
                upper_inclusive: false

      @spec new(any, any, keyword()) :: __MODULE__.t()
      def new(lower, upper, opts \\ []) do
        fields = Keyword.merge(opts, lower: lower, upper: upper)
        struct!(__MODULE__, fields)
      end

      @doc false
      @spec from_postgrex(Range.t()) :: __MODULE__.t()
      def from_postgrex(%Range{} = range), do: struct!(__MODULE__, Map.from_struct(range))

      @doc false
      @spec to_postgrex(__MODULE__.t()) :: Range.t()
      def to_postgrex(%__MODULE__{} = range), do: struct!(Range, Map.from_struct(range))

      @doc false
      def cast(nil), do: {:ok, nil}
      def cast(%Range{} = range), do: {:ok, from_postgrex(range)}
      def cast(%__MODULE__{} = range), do: {:ok, range}
      def cast(_), do: :error

      @doc false
      def load(nil), do: {:ok, nil}
      def load(%Range{} = range), do: {:ok, from_postgrex(range)}
      def load(_), do: :error

      @doc false
      def dump(nil), do: {:ok, nil}
      def dump(%__MODULE__{} = range), do: {:ok, to_postgrex(range)}
      def dump(_), do: :error

      defoverridable new: 2,
                     new: 3,
                     from_postgrex: 1,
                     to_postgrex: 1
    end
  end

  @doc false
  defmacro __before_compile__(env) do
    unless Module.defines?(env.module, {:type, 0}) do
      message = """
      function type/0 required by behaviour Ecto.Type is not implemented \
      (in module #{inspect(env.module)}).
      """

      IO.warn(message, Macro.Env.stacktrace(env))

      quote do
        @doc false
        def type, do: :not_a_valid_type
      end
    end
  end
end