lib/fussy/validators/date_time.ex

defmodule Fussy.Validators.DateTime do
  @behaviour Fussy.Validator

  defstruct naive: false, format: :iso8601

  alias Fussy.ValidationError

  @opaque t :: %__MODULE__{}

  @type timex_format :: String.t()

  @spec new(
          naive: boolean() | nil,
          format: :iso8601 | timex_format | nil
        ) :: __MODULE__.t()
  def new(args \\ []), do: struct!(%__MODULE__{}, args)

  def validate(%__MODULE__{} = v, term), do: validate(v, [], term)

  @impl true
  def validate(%__MODULE__{naive: false}, _path, %DateTime{} = dt) do
    {:ok, dt}
  end

  @impl true
  def validate(%__MODULE__{naive: false, format: :iso8601} = v, path, term)
      when is_binary(term) do
    case DateTime.from_iso8601(term) do
      {:ok, dt, _} ->
        {:ok, dt}

      {:error, _} ->
        error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{naive: false, format: format} = v, path, term)
      when is_binary(term) and is_binary(format) do
    case Timex.parse(term, format) do
      {:ok, %DateTime{} = dt} ->
        {:ok, dt}

      {:ok, _dt} ->
        error(v, path, term)

      {:error, _} ->
        error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{naive: true}, _path, %NaiveDateTime{} = dt) do
    {:ok, dt}
  end

  @impl true
  def validate(%__MODULE__{naive: true, format: :iso8601} = v, path, term) when is_binary(term) do
    case NaiveDateTime.from_iso8601(term) do
      {:ok, dt} ->
        {:ok, dt}

      {:error, _} ->
        error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{naive: true, format: format} = v, path, term)
      when is_binary(term) and is_binary(format) do
    case Timex.parse(term, format) do
      {:ok, %NaiveDateTime{} = dt} ->
        {:ok, dt}

      {:ok, _dt} ->
        error(v, path, term)

      {:error, _} ->
        error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{} = v, path, term), do: error(v, path, term)

  defp error(%__MODULE__{naive: true, format: :iso8601}, path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         msg: "must be a NaiveDateTime or an ISO8601 string",
         term: term,
         path: path
       }
     ]}
  end

  defp error(%__MODULE__{naive: true, format: format}, path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         msg: "must be a NaiveDateTime or a string matching format `#{format}`",
         term: term,
         path: path
       }
     ]}
  end

  defp error(%__MODULE__{naive: false, format: :iso8601}, path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         msg: "must be a DateTime or an ISO8601 string",
         term: term,
         path: path
       }
     ]}
  end

  defp error(%__MODULE__{naive: false, format: format}, path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         msg: "must be a DateTime or a string matching format `#{format}`",
         term: term,
         path: path
       }
     ]}
  end
end