Current section
Files
Jump to
Current section
Files
lib/icalendar/encoder.ex
defmodule ICalendar.Encoder do
alias ICalendar.RFC6868
import ICalendar, only: [__props__: 1]
@types %{
event: "VEVENT",
alarm: "VALARM",
calendar: "VCALENDAR",
todo: "VTODO",
journal: "VJOURNAL",
freebusy: "VFREEBUSY",
timezone: "VTIMEZONE",
standard: "STANDARD",
daylight: "DAYLIGHT",
}
@doc "Encode into iCal format."
def encode(obj) do
obj
|> encode_component
|> IO.iodata_to_binary
end
def encode_to_iodata(obj) do
encode_component(obj)
end
@doc "Encode a component."
@spec encode_component(component :: map) :: iodata
def encode_component(%{__type__: key} = component) do
[
"BEGIN:#{key}",
"\n",
# TODO: map drop is slow, use some form of a skip, probably reduce
Enum.map(Map.drop(component, [:__type__]), fn
{key, vals} when is_list(vals) -> Enum.map(vals, fn val -> encode_prop(key, val) end)
{key, val} -> encode_prop(key, val)
end),
"END:#{key}",
"\n",
]
end
defp encode_key(key) do
key
|> Atom.to_string()
|> String.upcase()
|> String.replace("_", "-")
end
@doc "Encode a property."
def encode_prop(_key, %{} = component) do
encode_component(component)
end
def encode_prop(key, data) when is_tuple(data) do
# shim: load the spec data
# TODO: make this neater
spec = __props__(key)
encode_prop(key, data, spec)
end
# HAXX: we skip the per line encode here, but this isn't too elegant
def encode_prop(key, {vals, params, type}, %{multi: delim}) when is_list(vals) do
{val, params} = Enum.reduce(vals, {[], params}, fn val, {vals, params} ->
case encode_val(val, type) do
{val, extra_params} ->
{[val | vals], Map.merge(params, extra_params)}
val ->
{[val | vals], params}
end
end)
[encode_kparam(key, params), ":", Enum.intersperse(val, delim), "\n"]
end
def encode_prop(key, {vals, params, type}, spec) when is_list(vals) do
Enum.map(vals, &encode_prop(key, {&1, params, type}))
end
def encode_prop(key, {val, params, type}, spec) do
# take any extra params the field encoding might have given
{val, params} = case encode_val(val, type) do
{val, extra_params} ->
{val, Map.merge(params, extra_params)}
val ->
{val, params}
end
[encode_kparam(key, params), ":", val, "\n"]
end
# TODO: work with structured vals, tuple
@doc "Encode a key together with parameters."
def encode_kparam(key, params) when params == %{}, do: encode_key(key)
def encode_kparam(key, params) do
[encode_key(key), ";", encode_params(params)]
end
@doc "Encode parameters."
@spec encode_params(params :: map) :: String.t
def encode_params(params) do
params
# TODO escape parameter vals (^)
|> Enum.map(fn {key, val} -> encode_key(key) <> "=" <> RFC6868.escape(val) end)
|> Enum.join(";")
end
# -----------
# TODO: move helper
defp zero_pad(val, count) when val >= 0 do
num = Integer.to_string(val)
:binary.copy("0", count - byte_size(num)) <> num
end
@doc "Encode a value."
def encode_val(vals, type) when is_tuple(vals) do
vals
|> Tuple.to_list()
|> Enum.map(&encode_val(&1, type))
|> Enum.join(";") # TODO configurable per field
end
def encode_val(val, :binary) do
# TODO for non base64?
{Base.encode64(val), %{encoding: "BASE64"}}
end
def encode_val(true, :boolean), do: "TRUE"
def encode_val(false, :boolean), do: "FALSE"
def encode_val(val, :cal_address), do: val
def encode_val(val, :date) do
zero_pad(val.year, 4) <> zero_pad(val.month, 2) <> zero_pad(val.day, 2)
end
# TODO: just match on the next case for when not == Etc/UTC
def encode_val(%{time_zone: "Etc/UTC"} = val, :date_time) do
date = encode_val(val, :date)
time = encode_val(val, :time)
date <> "T" <> time
end
def encode_val(%{time_zone: _time_zone} = val, :date_time) do
date = encode_val(val, :date)
{time, params} = encode_val(val, :time)
{
date <> "T" <> time,
params
}
end
def encode_val(val, :date_time) do
date = encode_val(val, :date)
time = encode_val(val, :time)
date <> "T" <> time
end
def encode_val(val, :duration) do
string = Timex.Format.Duration.Formatter.format(val)
if (val.seconds < 0) || (val.megaseconds < 0) || (val.microseconds < 0) do
"-" <> string
else
string
end
end
def encode_val(val, :float), do: to_string(val)
def encode_val(val, :integer), do: to_string(val)
def encode_val(val, :period) do
# TODO somehow store if period end is a duration or datetime
# so we can encode it again proper
""
end
def encode_val(val, :recur) do
#TODO:
""
end
@escape ~r/\\|;|,|\n/
def encode_val(val, :text) do
# TODO: optimize: only run the regex if string contains those chars
Regex.replace(@escape, val, fn
"\\" -> "\\\\"
";" -> "\\;"
"," -> "\\,"
"\n" -> "\\n"
v -> v
end)
end
def encode_val(%{time_zone: "Etc/UTC"} = val, :time) do
zero_pad(val.hour, 2) <> zero_pad(val.minute, 2) <> zero_pad(val.second, 2) <> "Z"
end
def encode_val(%{time_zone: time_zone} = val, :time) do
{
zero_pad(val.hour, 2) <> zero_pad(val.minute, 2) <> zero_pad(val.second, 2),
%{tzid: time_zone}
}
end
def encode_val(val, :time) do
zero_pad(val.hour, 2) <> zero_pad(val.minute, 2) <> zero_pad(val.second, 2)
end
def encode_val(val, :uri), do: val
# TODO once encoding is decided
def encode_val(val, :utc_offset), do: val
def encode_val(val, :unknown), do: val
end