lib/caldav_client/event.ex

defmodule CalDAVClient.Event do
  @moduledoc """
  Allows for managing events on the calendar server.
  """

  import CalDAVClient.HTTP.Error
  import CalDAVClient.Tesla

  @type t :: %__MODULE__{
          icalendar: String.t(),
          url: String.t(),
          etag: String.t()
        }

  @enforce_keys [:icalendar, :url, :etag]
  defstruct @enforce_keys

  @doc """
  Creates an event (see [RFC 4791, section 5.3.2](https://tools.ietf.org/html/rfc4791#section-5.3.2)).
  """
  @spec create(CalDAVClient.Client.t(), event_url :: String.t(), event_icalendar :: String.t()) ::
          {:ok, etag :: String.t() | nil} | {:error, any()}
  def create(caldav_client, event_url, event_icalendar) do
    # fail when event already exists
    headers = [{"If-None-Match", "*"}]

    case caldav_client
         |> make_tesla_client([
           CalDAVClient.Tesla.ContentTypeICalendarMiddleware,
           CalDAVClient.Tesla.ContentLengthMiddleware
         ])
         |> Tesla.put(event_url, event_icalendar, headers: headers) do
      {:ok, %Tesla.Env{status: code} = env} when code in [201, 204] ->
        etag = env |> Tesla.get_header("etag")
        {:ok, etag}

      {:ok, %Tesla.Env{status: code}} ->
        case code do
          412 -> {:error, :already_exists}
          _ -> {:error, reason_atom(code)}
        end

      {:error, _reason} = error ->
        error
    end
  end

  @doc """
  Updates a specific event (see [RFC 4791, section 5.3.2](https://tools.ietf.org/html/rfc4791#section-5.3.2)).

  ## Options
  * `etag` - a specific ETag used to ensure that the client overwrites the latest version of the event.
  """
  @spec update(
          CalDAVClient.Client.t(),
          event_url :: String.t(),
          event_icalendar :: String.t(),
          opts :: keyword()
        ) :: {:ok, etag :: String.t() | nil} | {:error, any()}
  def update(caldav_client, event_url, event_icalendar, opts \\ []) do
    case caldav_client
         |> make_tesla_client([
           CalDAVClient.Tesla.ContentTypeICalendarMiddleware,
           CalDAVClient.Tesla.ContentLengthMiddleware,
           {CalDAVClient.Tesla.IfMatchMiddleware, etag: opts[:etag]}
         ])
         |> Tesla.put(event_url, event_icalendar) do
      {:ok, %Tesla.Env{status: code} = env} when code in [201, 204] ->
        etag = env |> Tesla.get_header("etag")
        {:ok, etag}

      {:ok, %Tesla.Env{status: code}} ->
        case code do
          412 -> {:error, :bad_etag}
          _ -> {:error, reason_atom(code)}
        end

      {:error, _reason} = error ->
        error
    end
  end

  @doc """
  Deletes a specific event.

  ## Options
  * `etag` - a specific ETag used to ensure that the client overwrites the latest version of the event.
  """
  @spec delete(CalDAVClient.Client.t(), event_url :: String.t(), opts :: keyword()) ::
          :ok | {:error, any()}
  def delete(caldav_client, event_url, opts \\ []) do
    case caldav_client
         |> make_tesla_client([
           CalDAVClient.Tesla.ContentTypeICalendarMiddleware,
           CalDAVClient.Tesla.ContentLengthMiddleware,
           {CalDAVClient.Tesla.IfMatchMiddleware, etag: opts[:etag]}
         ])
         |> Tesla.delete(event_url) do
      {:ok, %Tesla.Env{status: code}} ->
        case code do
          204 -> :ok
          412 -> {:error, :bad_etag}
          _ -> {:error, reason_atom(code)}
        end

      {:error, _reason} = error ->
        error
    end
  end

  @doc """
  Returns a specific event in the iCalendar format along with its ETag.
  """
  @spec get(CalDAVClient.Client.t(), event_url :: String.t()) ::
          {:ok, icalendar :: String.t(), etag :: String.t()} | {:error, any()}
  def get(caldav_client, event_url) do
    case caldav_client
         |> make_tesla_client()
         |> Tesla.get(event_url) do
      {:ok, %Tesla.Env{status: 200, body: icalendar} = env} ->
        etag = env |> Tesla.get_header("etag")
        {:ok, icalendar, etag}

      {:ok, %Tesla.Env{status: code}} ->
        {:error, reason_atom(code)}

      {:error, _reason} = error ->
        error
    end
  end

  @doc """
  Returns an event with the specified UID property
  (see [RFC 4791, section 7.8.6](https://tools.ietf.org/html/rfc4791#section-7.8.6)).
  """
  @spec find_by_uid(CalDAVClient.Client.t(), calendar_url :: String.t(), event_uid :: String.t()) ::
          {:ok, t()} | {:error, any()}
  def find_by_uid(caldav_client, calendar_url, event_uid) do
    request_xml = CalDAVClient.XML.Builder.build_retrieval_of_event_by_uid_xml(event_uid)

    case caldav_client |> get_events_by_xml(calendar_url, request_xml) do
      {:ok, [event]} -> {:ok, event}
      {:ok, []} -> {:error, :not_found}
      {:ok, _events} -> {:error, :multiple_found}
      {:error, _reason} = error -> error
    end
  end

  @doc """
  Retrieves all events or its occurrences within a specific time range
  (see [RFC 4791, section 7.8.1](https://tools.ietf.org/html/rfc4791#section-7.8.1)).

  ## Options
  * `expand` - if `true`, recurring events will be expanded to occurrences, defaults to `false`.
  """
  @spec get_events(
          CalDAVClient.Client.t(),
          calendar_url :: String.t(),
          from :: DateTime.t(),
          to :: DateTime.t(),
          opts :: keyword()
        ) :: {:ok, [t()]} | {:error, any()}
  def get_events(caldav_client, calendar_url, from, to, opts \\ []) do
    request_xml = CalDAVClient.XML.Builder.build_retrieval_of_events_xml(from, to, opts)
    caldav_client |> get_events_by_xml(calendar_url, request_xml)
  end

  @doc """
  Retrieves all events or its occurrences having an VALARM within a specific time range
  (see [RFC 4791, section 7.8.5](https://tools.ietf.org/html/rfc4791#section-7.8.5)).

  ## Options
  * `expand` - if `true`, recurring events will be expanded to occurrences, defaults to `false`.
  * `event_from` - start of time range for events or occurrences, defaults to `0000-00-00T00:00:00Z`.
  * `event_to` - end of time range for events or occurrences, defaults to `9999-12-31T23:59:59Z`.
  """
  @spec get_events_by_alarm(
          CalDAVClient.Client.t(),
          calendar_url :: String.t(),
          from :: DateTime.t(),
          to :: DateTime.t(),
          opts :: keyword()
        ) ::
          {:ok, [t()]} | {:error, any()}
  def get_events_by_alarm(caldav_client, calendar_url, from, to, opts \\ []) do
    request_xml =
      CalDAVClient.XML.Builder.build_retrieval_of_events_having_alarm_xml(from, to, opts)

    caldav_client |> get_events_by_xml(calendar_url, request_xml)
  end

  @doc """
  Retrieves all occurrences of events for given XML request body.
  """
  @spec get_events_by_xml(
          CalDAVClient.Client.t(),
          calendar_url :: String.t(),
          request_xml :: String.t()
        ) ::
          {:ok, [t()]} | {:error, any()}
  def get_events_by_xml(caldav_client, calendar_url, request_xml) do
    case caldav_client
         |> make_tesla_client([
           CalDAVClient.Tesla.ContentTypeXMLMiddleware,
           CalDAVClient.Tesla.ContentLengthMiddleware
         ])
         |> Tesla.request(
           method: :report,
           url: calendar_url,
           body: request_xml,
           headers: [{"Depth", "1"}],
           opts: [pre_auth_method: :get]
         ) do
      {:ok, %Tesla.Env{status: 207, body: response_xml}} ->
        events = response_xml |> CalDAVClient.XML.Parser.parse_events()
        {:ok, events}

      {:ok, %Tesla.Env{status: code}} ->
        {:error, reason_atom(code)}

      {:error, _reason} = error ->
        error
    end
  end
end