lib/kvasir/type/timestamp.ex

defmodule Kvasir.Type.Timestamp do
  @moduledoc ~S"""
  A timestamp containing date and time.

  By default the timestamp is UTC only.
  """
  use Kvasir.Type

  @impl Kvasir.Type
  def parse(timestamp, opts \\ [])

  def parse(timestamp = %t{}, _opts) when t in [UTCDateTime, DateTime, NaiveDateTime],
    do: {:ok, timestamp}

  def parse(timestamp, opts) when is_binary(timestamp) do
    case opts[:format] do
      nil -> utc_from_8601(timestamp)
      UTCDateTime -> utc_from_8601(timestamp)
      DateTime -> dt_from_8601(timestamp)
      NaiveDateTime -> ndt_from_8601(timestamp)
    end
  end

  def parse(timestamp, opts) when is_integer(timestamp) do
    case opts[:format] do
      nil -> utc_from_unix(timestamp)
      UTCDateTime -> utc_from_unix(timestamp)
      DateTime -> dt_from_unix(timestamp)
      NaiveDateTime -> ndt_from_unix(timestamp)
    end
  end

  def parse(_, _opts), do: {:error, :invalid_timestamp}

  @impl Kvasir.Type
  def dump(data, opts \\ [])
  def dump(data = %UTCDateTime{}, _opts), do: {:ok, UTCDateTime.to_rfc3339(data)}
  def dump(data = %DateTime{}, _opts), do: {:ok, DateTime.to_iso8601(data)}
  def dump(data = %NaiveDateTime{}, _opts), do: {:ok, NaiveDateTime.to_iso8601(data) <> "Z"}

  ### Helpers ###

  defp dt_from_8601(timestamp) do
    case NaiveDateTime.from_iso8601(timestamp) do
      {:ok, t, _} -> {:ok, t}
      _ -> {:error, :invalid_timestamp}
    end
  end

  defp ndt_from_8601(timestamp) do
    with {:error, _} <- NaiveDateTime.from_iso8601(timestamp), do: {:error, :invalid_timestamp}
  end

  defp utc_from_8601(timestamp) do
    with {:error, _} <- UTCDateTime.from_iso8601(timestamp), do: {:error, :invalid_timestamp}
  end

  defp dt_from_unix(unix), do: DateTime.from_unix(unix)
  defp ndt_from_unix(unix), do: NaiveDateTime.add(~N[1970-01-01 00:00:00], unix)

  defp utc_from_unix(unix),
    do: ~N[1970-01-01 00:00:00] |> NaiveDateTime.add(unix) |> UTCDateTime.from_naive()
end