Packages

Parse and manipulate iCalendar (RFC5545) and vCard objects (RFC6350)

Current section

Files

Jump to
vobject lib icalendar util deserialize.ex
Raw

lib/icalendar/util/deserialize.ex

defmodule ICalendar.Util.Deserialize do
@moduledoc """
Deserialize ICalendar Strings into Event structs
"""
alias ICalendar.Event
alias ICalendar.Property
alias ICalendar.RRULE
def invert_map(map = %{}) do
map
|> Enum.reduce(%{}, fn({key, value}, acc) ->
Map.put(acc, value, key)
end)
end
def build_event(lines) when is_list(lines) do
lines
|> Enum.map(&retrieve_kvs/1)
|> Enum.reduce(%Event{}, &parse_attr/2)
|> validate
end
def validate(event = %ICalendar.Event{}) do
if ICalendar.Event.valid(event) do
{:ok, event}
else
{:error, event.errors}
end
end
@doc ~S"""
This function extracts the key and value parts from each line of a iCalendar
string.
iex> ICalendar.Util.Deserialize.retrieve_kvs("lorem:ipsum")
%ICalendar.Property{key: "LOREM", params: %{}, value: "ipsum"}
"""
def retrieve_kvs(line) do
# Split Line up into key and value
[key, value] = String.split(line, ":", parts: 2, trim: true)
[key, params] = retrieve_params(key)
%Property{
key: String.upcase(key),
value: value,
params: params
}
end
@doc ~S"""
This function extracts parameter data from a key in an iCalendar string.
iex> ICalendar.Util.Deserialize.retrieve_params(
...> "DTSTART;TZID=America/Chicago")
["DTSTART", %{"TZID" => "America/Chicago"}]
It should be able to handle multiple parameters per key:
iex> ICalendar.Util.Deserialize.retrieve_params(
...> "KEY;LOREM=ipsum;DOLOR=sit")
["KEY", %{"LOREM" => "ipsum", "DOLOR" => "sit"}]
"""
def retrieve_params(key) do
[key | params] = String.split(key, ";", trim: true)
params =
params
|> Enum.reduce(%{}, fn(param, acc) ->
[key, val] = String.split(param, "=", parts: 2, trim: true)
Map.merge(acc, %{key => val})
end)
[key, params]
end
def parse_attr(
%Property{key: "DESCRIPTION", value: description},
acc
) do
%{acc | description: desanitized(description)}
end
def parse_attr(
%Property{key: "DTSTART", value: dtstart, params: params},
acc
) do
case to_date(dtstart, params) do
{:ok, timestamp} -> %{acc | dtstart: timestamp}
{:error, term} ->
%{acc | errors: ["DTSTART: #{term}" | acc.errors]}
end
end
def parse_attr(
%Property{key: "DTEND", value: dtend, params: params},
acc
) do
case to_date(dtend, params) do
{:ok, timestamp} -> %{acc | dtend: timestamp}
{:error, term} ->
%{acc | errors: ["DTEND: #{term}" | acc.errors]}
end
end
def parse_attr(
%Property{key: "SUMMARY", value: summary},
acc
) do
%{acc | summary: desanitized(summary)}
end
def parse_attr(
%Property{key: "LOCATION", value: location},
acc
) do
%{acc | location: desanitized(location)}
end
def parse_attr(
%Property{key: "RRULE", value: rrule},
acc
) do
case RRULE.deserialize(rrule) do
{:ok, rrule} -> %{acc | rrule: rrule}
{:error, term} ->
%{acc | errors: ["RRULE: #{term}" | acc.errors]}
end
end
def parse_attr(
%Property{key: "COMMENT", value: comment},
acc
) do
%{acc | comment: desanitized(comment)}
end
def parse_attr(
%Property{key: "STATUS", value: status},
acc
) do
%{acc | status: status |> desanitized() |> String.downcase()}
end
def parse_attr(
%Property{key: "CATEGORIES", value: categories},
acc
) do
%{acc | categories: String.split(desanitized(categories), ",")}
end
def parse_attr(
%Property{key: "CLASS", value: class},
acc
) do
%{acc | class: class |> desanitized() |> String.downcase()}
end
def parse_attr(
%Property{key: "GEO", value: geo},
acc
) do
%{acc | geo: to_geo(geo)}
end
def parse_attr(_, acc), do: acc
@doc ~S"""
This function is designed to parse iCal datetime strings into erlang dates.
It should be able to handle dates from the past:
iex> {:ok, date} = ICalendar.Util.Deserialize.to_date("19930407T153022Z")
...> Timex.to_erl(date)
{{1993, 4, 7}, {15, 30, 22}}
As well as the future:
iex> {:ok, date} = ICalendar.Util.Deserialize.to_date("39930407T153022Z")
...> Timex.to_erl(date)
{{3993, 4, 7}, {15, 30, 22}}
And should return error for incorrect dates:
iex> ICalendar.Util.Deserialize.to_date("1993/04/07")
{:error, "Expected `2 digit month` at line 1, column 5."}
It should handle timezones from the Olson Database:
iex> {:ok, date} = ICalendar.Util.Deserialize.to_date("19980119T020000",
...> %{"TZID" => "America/Chicago"})
...> [Timex.to_erl(date), date.time_zone]
[{{1998, 1, 19}, {2, 0, 0}}, "America/Chicago"]
"""
def to_date(date_string, %{"TZID" => timezone}) do
date_string =
case String.last(date_string) do
"Z" -> date_string
_ -> date_string <> "Z"
end
Timex.parse(date_string <> timezone, "{YYYY}{0M}{0D}T{h24}{m}{s}Z{Zname}")
end
def to_date(date_string, %{}) do
to_date(date_string, %{"TZID" => "Etc/UTC"})
end
def to_date(date_string) do
to_date(date_string, %{"TZID" => "Etc/UTC"})
end
defp to_geo(geo) do
geo
|> desanitized()
|> String.split(";")
|> Enum.map(fn x -> Float.parse(x) end)
|> Enum.map(fn {x, _} -> x end)
|> List.to_tuple()
end
@doc ~S"""
This function should strip any sanitization that has been applied to content
within an iCal string.
iex> ICalendar.Util.Deserialize.desanitized(~s(lorem\\, ipsum))
"lorem, ipsum"
"""
def desanitized(string) do
string
|> String.replace(~s(\\), "")
end
end