Packages

RFC 7231 HTTP date parsing/formatting library

Current section

Files

Jump to
http_date lib http_date.ex
Raw

lib/http_date.ex

defmodule HTTPDate do
@type format :: :imf_fixdate | :asctime | :rfc850
@type component :: :day | :month | :year | :hour | :minute | :second
defmodule ParseError do
defexception [:type, :date, :calendar]
@impl Exception
def exception({ type, date, calendar }) do
%ParseError{
type: type,
date: date,
calendar: calendar
}
end
defp readable_format(:imf_fixdate), do: "IMF-fixdate"
defp readable_format(:asctime), do: "asctime"
defp readable_format(:rfc850), do: "RFC 850"
@impl Exception
def message(%{ type: :invalid, date: date, calendar: calendar }), do: "invalid date for calendar (#{inspect calendar}): #{inspect date}"
def message(%{ type: :unknown_format, date: date }), do: "unknown format: #{inspect date}"
def message(%{ type: { format, component }, date: date }), do: "invalid #{component} component in #{readable_format(format)} format: #{inspect date}"
end
@doc """
Parse an HTTP-date (RFC 7231).
This supports both the preferred format:
Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
As well as the obsolete formats:
Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
By default the parsed date will be validated to ensure that it is correct. If this
validation is not desired it can be disabled by passing `false` to the `:validate`
option.
By default it uses `Calendar.ISO`, however this can be changed by passing another
calendar to the `:calendar` option.
As the obsolete RFC 850 format only allows for the last 2 year digits, it is
assumed these digits belong to the current year. If a specific base year is
desired, pass the year to the `:base_year` option.
iex> HTTPDate.parse("Sun, 06 Nov 1994 08:49:37 GMT")
{ :ok, elem(DateTime.from_iso8601("1994-11-06 08:49:37Z"), 1) }
iex> HTTPDate.parse("Sunday, 06-Nov-94 08:49:37 GMT", base_year: 1900)
{ :ok, elem(DateTime.from_iso8601("1994-11-06 08:49:37Z"), 1) }
iex> HTTPDate.parse("Sun Nov 6 08:49:37 1994")
{ :ok, elem(DateTime.from_iso8601("1994-11-06 08:49:37Z"), 1) }
iex> HTTPDate.parse("Mon, 06 Nov 1994 08:49:37 GMT")
{ :error, :invalid }
iex> HTTPDate.parse("Mon, 06 Nov 1994 08:49:37 GMT", validate: false)
{ :ok, elem(DateTime.from_iso8601("1994-11-06 08:49:37Z"), 1) }
"""
@spec parse(String.t, Keyword.t) :: { :ok, DateTime.t } | { :error, :invalid | :unknown_format | { format, component } }
def parse(date, opts \\ []) do
calendar = opts[:calendar] || Calendar.ISO
case { HTTPDate.Parser.parse_date(date, calendar, opts[:base_year]), Keyword.get(opts, :validate, true) } do
{ { :ok, { date, weekday } }, true } ->
cond do
not date.calendar.valid_date?(date.year, date.month, date.day) -> { :error, :invalid }
not date.calendar.valid_time?(date.hour, date.minute, date.second, date.microsecond) -> { :error, :invalid }
weekday != date.calendar.day_of_week(date.year, date.month, date.day) -> { :error, :invalid }
true -> { :ok, date }
end
{ { :ok, { date, _ } }, _ } -> { :ok, date }
{ error, _ } -> error
end
end
@doc """
Parse an HTTP-date (RFC 7231).
Raises an `HTTPDate.ParseError` if the date could not be parsed.
For more details see `HTTPDate.parse/2`.
iex> HTTPDate.parse!("Sun, 06 Nov 1994 08:49:37 GMT")
elem(DateTime.from_iso8601("1994-11-06 08:49:37Z"), 1)
"""
@spec parse!(String.t, Keyword.t) :: DateTime.t | no_return
def parse!(date, opts \\ []) do
case parse(date, opts) do
{ :ok, date } -> date
{ :error, type } -> raise ParseError, { type, date, opts[:calendar] || Calendar.ISO }
end
end
@doc """
Format as an HTTP-date (RFC 7231).
By default it produces IMF-fixdate formatted dates, this can be changed by setting
the `:format` option to the desired format, see `t:format/0`.
iex> HTTPDate.format(HTTPDate.parse!("Sun, 06 Nov 1994 08:49:37 GMT"))
"Sun, 06 Nov 1994 08:49:37 GMT"
iex> HTTPDate.format(HTTPDate.parse!("Sunday, 06-Nov-94 08:49:37 GMT", base_year: 1900), format: :rfc850)
"Sunday, 06-Nov-94 08:49:37 GMT"
iex> HTTPDate.format(HTTPDate.parse!("Sun Nov 6 08:49:37 1994"), format: :asctime)
"Sun Nov 6 08:49:37 1994"
"""
@spec format(DateTime.t, Keyword.t) :: String.t
def format(date, opts \\ []), do: HTTPDate.Formatter.format_date(date, opts[:format] || :imf_fixdate)
end