Added todo comments for later

Added todo comments for later

defmodule Wizard.Sharepoint do
  require Logger

  import Ecto.Query, warn: false
  import Ecto.Changeset, only: [fetch_field: 2]
  alias Ecto.Multi
  alias Wizard.Repo

  alias Wizard.{Feeds, User}
  alias Wizard.Feeds.Feed
  alias Wizard.Sharepoint.{Authorization, Drive, Events, Item, Service, Site}
  alias Wizard.Sharepoint.Api.{Authentication, Sites}

  @type transaction_result :: {:ok, any} | {:error, any} | {:error, any, any, any}
  @type parents :: %{optional(String.t) => Item.t}

  @spec authorize_url(String.t) :: String.t
  def authorize_url(state),
    do: Authentication.authorize_url(state)

  @spec authorize_sharepoints(String.t) :: transaction_result
  def authorize_sharepoints(code) do
    case Authentication.authorize_sharepoints(code) do
      {:ok, user_info, services_info, authorizations_info} ->
        upsert_user_and_services(user_info, services_info, authorizations_info)
      error ->
        error
    end
  end

  @spec upsert_user_and_services(map, [map], [map]) :: transaction_result
  def upsert_user_and_services(user_info, services_info, authorizations_info) do
    build_upsert_user_and_services(user_info, services_info, authorizations_info)
    |> Repo.transaction()
  end

  @spec reauthorize(Service.t, User.t) :: {:ok, Authorization.t} | {:error, Ecto.Changeset.t} | {:error, atom}
  def reauthorize(%Service{} = service, %User{} = user) do
    case Repo.get_by(Authorization, service_id: service.id, user_id: user.id) do
      nil -> {:error, :authorization_not_found}
      auth ->
        auth = %{auth | service: service}
        reauthorize(auth)
    end
  end

  @spec reauthorize(Authorization.t) :: {:ok, Authorization.t} | {:error, Ecto.Changeset.t} | {:error, atom}
  def reauthorize(%Authorization{service: %Service{} = service} = auth) do
    case Authentication.reauthorize_sharepoint_service(service, auth.refresh_token) do
      {:ok, attrs} ->
        Authorization.refresh_changeset(auth, attrs)
        |> Repo.update()
      error ->
        error
    end
  end

  @spec remotely_search_sites(Service.t, User.t, String.t) :: [map] | {:error, atom}
  def remotely_search_sites(service, user, query) do
    case Repo.get_by(Authorization, service_id: service.id, user_id: user.id) do
      nil -> {:error, :unauthorized}
      auth -> Sites.search(auth, service, query)
    end
  end

  @spec remotely_list_drives(Site.t, User.t) :: [map] | {:error, atom}
  def remotely_list_drives(site, user) do
    site = Repo.preload(site, :service)

    case Repo.get_by(Authorization, service_id: site.service.id, user_id: user.id) do
      nil -> {:error, :unauthorized}
      auth -> Sites.drives(auth, site)
    end
  end

  @site_conflict_query from s in Site,
                         update: [set: [
                           url: fragment("EXCLUDED.url"),
                           hostname: fragment("EXCLUDED.hostname"),
                           title: fragment("EXCLUDED.title"),
                           description: fragment("EXCLUDED.description")
                         ]]

  @site_conflict_options [on_conflict: @site_conflict_query,
                          conflict_target: :remote_id]

  @spec upsert_site(map, [service: Service.t]) :: {:ok, Site.t} | {:error, Ecto.Changeset.t}
  def upsert_site(attrs, [service: service]) do
    Site.changeset(attrs, service: service)
    |> Repo.insert(@site_conflict_options)
  end

  @drive_conflict_query from d in Drive,
                          update: [set: [
                            name: fragment(""),
                            type: fragment(""),
                            url: fragment(""),
                            delta_link: fragment(""),
                          ]]

  @drive_conflict_options [on_conflict: @drive_conflict_query,
                           conflict_target: :remote_id]

  @spec upsert_drive(map, [site: Site.t]) :: {:ok, Drive.t} | {:error, Ecto.Changeset.t}
  def upsert_drive(attrs, [site: site]) do
    Drive.changeset(attrs, site: site)
    |> Repo.insert(@drive_conflict_options)
  end

  @spec update_drive(Drive.t, [delta_link: String.t]) :: {:ok, Drive.t} | {:error, Ecto.Changeset.t}
  def update_drive(drive, [delta_link: delta_link]) do
    drive
    |> Drive.update_delta_link_changeset(delta_link)
    |> Repo.update()
  end

  @user_conflict_query from u in User,
                         update: [set: [
                           name: fragment("EXCLUDED.name")
                         ]]

  @user_conflict_options [on_conflict: @user_conflict_query,
                          conflict_target: :email]

  defp upsert_user(multi, user_info) do
    changeset = User.changeset(user_info)
    Multi.insert(multi, :user, changeset, @user_conflict_options)
  end

  @service_conflict_query from s in Service,
                            update: [set: [
                              title: fragment("EXCLUDED.title")
                            ]]

  @service_conflict_options [on_conflict: @service_conflict_query,
                             conflict_target: :resource_id]

  @spec upsert_services([map]) :: {:ok, [Service.t]} | {:error, any}
  defp upsert_services(infos) do
    case Multi.new |> upsert_services(infos) |> Repo.transaction() do
      {:ok, changes} ->
        services = for {{:service, _}, service} <- changes, do: service
        {:ok, services}
      error ->
        error
    end
  end

  @spec upsert_services(Multi.t, [map]) :: Multi.t
  defp upsert_services(multi, [info | infos]) do
    changeset = Service.changeset(info)
    {_, resource_id} = fetch_field(changeset, :resource_id)
    name = {:service, resource_id}

    multi
    |> Multi.insert(name, changeset, @service_conflict_options)
    |> upsert_services(infos)
  end

  defp upsert_services(multi, []), do: multi

  @spec upsert_authorizations(%{services: [Service.t], user: User.t}, [map]) :: {:ok, [Authorization.t]} | {:error, any}
  defp upsert_authorizations(%{services: services, user: user}, infos) do
    case Multi.new |> upsert_authorizations(user, services, infos) |> Repo.transaction() do
      {:ok, changes} ->
        auths = for {{:authorization, _}, authorization} <- changes, do: authorization
        {:ok, auths}
      error ->
        error
    end
  end

  @authorization_conflict_query from a in Authorization,
                                  update: [set: [
                                    access_token: fragment("EXCLUDED.access_token"),
                                    refresh_token: fragment("EXCLUDED.refresh_token")
                                  ]]

  @authorization_conflict_options [on_conflict: @authorization_conflict_query,
                                   conflict_target: [:user_id, :service_id]]

  @spec upsert_authorizations(Multi.t, User.t, [Service.t], [map]) :: Multi.t
  defp upsert_authorizations(multi, user, services, [info | infos]) do
    case Enum.find(services, &(&1.resource_id == Map.get(info, :resource_id))) do
      nil ->
        Multi.error(multi,
                    {:authorization, SecureRandom.hex()},
                    :missing_service_for_authorization_resource_id)
      service ->
        name = {:authorization, service.resource_id}

        changeset = Authorization.changeset(info, user: user, service: service)

        multi
        |> Multi.insert(name, changeset, @authorization_conflict_options)
        |> upsert_authorizations(user, services, infos)
    end
  end

  defp upsert_authorizations(multi, _, _, []), do: multi

  @spec build_upsert_user_and_services(map, [map], [map]) :: Multi.t
  defp build_upsert_user_and_services(user_info, services_info, authorizations_info) do
    Multi.new
    |> upsert_user(user_info)
    |> Multi.run(:services, fn _ -> upsert_services(services_info) end)
    |> Multi.run(:authorizations, &(upsert_authorizations(&1, authorizations_info)))
  end

  @spec upsert_remote_item(map, [drive: Drive.t, feed: Feed.t, parents: parents]) :: {:ok, any, parents} | {:error, any} | {:error, any, any, any}
  def upsert_remote_item(info, [drive: drive, feed: feed, parents: parents]) do
    attrs = Item.parse_remote(info)

    res = Multi.new
          |> insert_user_for_item_if_not_exists(attrs)
          |> upsert_item_with_parent(attrs, drive: drive, parents: parents)
          |> upsert_item_event(feed: feed)
          |> Repo.transaction()

    case res do
      {:ok, %{item: item} = result} ->
        {:ok, result, Map.put(parents, item.remote_id, item)}
      error ->
        error
    end
  end

  @other_user_conflict_query from u in User,
                               update: [set: [
                                 name: fragment("EXCLUDED.name")
                               ]]

  @other_user_conflict_options [on_conflict: @other_user_conflict_query,
                                conflict_target: :email,
                                returning: true]

  @spec insert_user_for_item_if_not_exists(Multi.t, map) :: Multi.t
  defp insert_user_for_item_if_not_exists(multi, %{user: nil}),
    do: multi

  defp insert_user_for_item_if_not_exists(multi, %{user: user_attrs}) do
    Multi.run(multi, :user, fn _ ->
      User.changeset(user_attrs)
      |> Repo.insert(@other_user_conflict_options)
    end)
  end

  @spec upsert_item_with_parent(Multi.t, map, [drive: Drive.t, parents: parents]) :: Multi.t
  defp upsert_item_with_parent(multi, %{parent_remote_id: nil} = item_attrs, [drive: drive, parents: _parents]) do
    multi
    |> upsert_item(item_attrs, drive: drive)
  end

  defp upsert_item_with_parent(multi, item_attrs, [drive: drive, parents: parents]) do
    case Map.fetch(parents, item_attrs.parent_remote_id) do
      {:ok, parent} ->
        multi
        |> upsert_item(item_attrs, drive: drive, parent: parent)
      :error ->
        Logger.error "cannot find parent for #{inspect({:item, item_attrs})}"
        multi
        |> Multi.error(:item, :cannot_find_parent_for_item)
    end
  end

  @spec item_changeset(map, [drive: Drive.t, parent: Item.t] | [drive: Drive.t]) :: Ecto.Changeset.t
  defp item_changeset(attrs, [drive: drive]),
    do: Item.changeset(attrs, drive: drive)

  defp item_changeset(attrs, [drive: drive, parent: parent]),
    do: Item.changeset(attrs, drive: drive, parent: parent)

  @item_conflict_query from i in Item,
                         update: [set: [
                           name: fragment("EXCLUDED.name"),
                           type: fragment("EXCLUDED.type"),
                           last_modified_at: fragment("EXCLUDED.last_modified_at"),
                           size: fragment("EXCLUDED.size"),
                           url: fragment("EXCLUDED.url"),
                           parent_id: fragment("EXCLUDED.parent_id")
                         ]]

  @item_conflict_options [on_conflict: @item_conflict_query,
                          conflict_target: [:remote_id, :drive_id],
                          returning: true]

  @spec upsert_item(Multi.t, map, [drive: Drive.t, parent: Item.t] | [drive: Drive.t]) :: Multi.t
  defp upsert_item(multi, attrs, opts) do
    Multi.run(multi, :item, fn _ ->
      item_changeset(attrs, opts)
      |> Repo.insert(@item_conflict_options)
    end)
  end

  # TODO: detect if it's an update or an insert
  @spec upsert_item_event(Multi.t, [feed: Feed.t]) :: Multi.t
  defp upsert_item_event(multi, [feed: feed]) do
    Multi.run(multi, :event, &upsert_item_event_multi_body(&1, feed))
  end

  defp upsert_item_event_multi_body(%{item: item, user: user}, %Feed{} = feed) do
    if Events.should_emit_event?(item, user) do
      Events.prepare_item_create_event(item, user)
      |> Keyword.merge([feed: feed])
      |> Feeds.upsert_event()
    else
      {:ok, nil}
    end
  end
  defp upsert_item_event_multi_body(_, _), do: {:ok, nil}

  @spec discover_parents([map], [drive: Drive.t]) :: parents
  def discover_parents([], _), do: %{}

  def discover_parents(infos, [drive: %{id: drive_id}]) do
    parent_ids = infos
                 |> Enum.map(&Item.assoc_remote_parent_remote_id/1)
                 |> Enum.drop_while(&is_nil/1)

    query = from i in Item,
              where: i.remote_id in ^parent_ids,
              where: i.drive_id == ^drive_id

    items = query |> Repo.all()

    for item <- items, into: %{}, do: {item.remote_id, item}
  end

  @spec upsert_or_delete_remote_items([map], [drive: Drive.t, feed: Feed.t]) :: transaction_result
  def upsert_or_delete_remote_items(infos, [drive: drive, feed: feed]) do
    deletes = for info <- infos, Map.has_key?(info, "deleted"), do: info
    upserts = infos -- deletes
    parents = discover_parents(upserts, drive: drive)

    delete_remote_items(deletes, drive: drive, feed: feed)
    upsert_remote_items(upserts, drive: drive, feed: feed, parents: parents)
  end

  @spec delete_remote_items([map], [drive: Drive.t, feed: Feed.t]) :: {integer, nil | [term]}
  defp delete_remote_items([], _), do: {0, []}

  # TODO: loop through and delete each one so we can do an event for it
  defp delete_remote_items(infos, [drive: %{id: drive_id}, feed: _feed]) do
    now = DateTime.utc_now()
    remote_ids = for info <- infos, Map.has_key?(info, "id"), do: info["id"]
    query = from i in Item,
              where: i.remote_id in ^remote_ids,
              where: i.drive_id == ^drive_id,
              update: [set: [deleted_at: ^now]]

    Repo.update_all(query, [])
  end

  @spec upsert_remote_items([map], [drive: Drive.t, feed: Feed.t, parents: parents]) :: :ok | {:error, any}
  defp upsert_remote_items([info | infos], [drive: drive, feed: feed, parents: parents]) do
    case upsert_remote_item(info, drive: drive, feed: feed, parents: parents) do
      {:ok, _, parents} ->
        upsert_remote_items(infos, drive: drive, feed: feed, parents: parents) # NOTE: recurse
      {:error, _} = error ->
        error
      {:error, name, msg, info} ->
        {:error, {name, msg, info}}
    end
  end

  defp upsert_remote_items([], _), do: :ok # NOTE: done
end

defmodule Wizard.Sharepoint do
  require Logger

  import Ecto.Query, warn: false
  import Ecto.Changeset, only: [fetch_field: 2]
  alias Ecto.Multi
  alias Wizard.Repo

  alias Wizard.{Feeds, User}
  alias Wizard.Feeds.Feed
  alias Wizard.Sharepoint.{Authorization, Drive, Events, Item, Service, Site}
  alias Wizard.Sharepoint.Api.{Authentication, Sites}

  @type transaction_result :: {:ok, any} | {:error, any} | {:error, any, any, any}
  @type parents :: %{optional(String.t) => Item.t}

  @spec authorize_url(String.t) :: String.t
  def authorize_url(state),
    do: Authentication.authorize_url(state)

  @spec authorize_sharepoints(String.t) :: transaction_result
  def authorize_sharepoints(code) do
    case Authentication.authorize_sharepoints(code) do
      {:ok, user_info, services_info, authorizations_info} ->
        upsert_user_and_services(user_info, services_info, authorizations_info)
      error ->
        error
    end
  end

  @spec upsert_user_and_services(map, [map], [map]) :: transaction_result
  def upsert_user_and_services(user_info, services_info, authorizations_info) do
    build_upsert_user_and_services(user_info, services_info, authorizations_info)
    |> Repo.transaction()
  end

  @spec reauthorize(Service.t, User.t) :: {:ok, Authorization.t} | {:error, Ecto.Changeset.t} | {:error, atom}
  def reauthorize(%Service{} = service, %User{} = user) do
    case Repo.get_by(Authorization, service_id: service.id, user_id: user.id) do
      nil -> {:error, :authorization_not_found}
      auth ->
        auth = %{auth | service: service}
        reauthorize(auth)
    end
  end

  @spec reauthorize(Authorization.t) :: {:ok, Authorization.t} | {:error, Ecto.Changeset.t} | {:error, atom}
  def reauthorize(%Authorization{service: %Service{} = service} = auth) do
    case Authentication.reauthorize_sharepoint_service(service, auth.refresh_token) do
      {:ok, attrs} ->
        Authorization.refresh_changeset(auth, attrs)
        |> Repo.update()
      error ->
        error
    end
  end

  @spec remotely_search_sites(Service.t, User.t, String.t) :: [map] | {:error, atom}
  def remotely_search_sites(service, user, query) do
    case Repo.get_by(Authorization, service_id: service.id, user_id: user.id) do
      nil -> {:error, :unauthorized}
      auth -> Sites.search(auth, service, query)
    end
  end

  @spec remotely_list_drives(Site.t, User.t) :: [map] | {:error, atom}
  def remotely_list_drives(site, user) do
    site = Repo.preload(site, :service)

    case Repo.get_by(Authorization, service_id: site.service.id, user_id: user.id) do
      nil -> {:error, :unauthorized}
      auth -> Sites.drives(auth, site)
    end
  end

  @site_conflict_query from s in Site,
                         update: [set: [
                           url: fragment("EXCLUDED.url"),
                           hostname: fragment("EXCLUDED.hostname"),
                           title: fragment("EXCLUDED.title"),
                           description: fragment("EXCLUDED.description")
                         ]]

  @site_conflict_options [on_conflict: @site_conflict_query,
                          conflict_target: :remote_id]

  @spec upsert_site(map, [service: Service.t]) :: {:ok, Site.t} | {:error, Ecto.Changeset.t}
  def upsert_site(attrs, [service: service]) do
    Site.changeset(attrs, service: service)
    |> Repo.insert(@site_conflict_options)
  end

  @drive_conflict_query from d in Drive,
                          update: [set: [
                            name: fragment(""),
                            type: fragment(""),
                            url: fragment(""),
                            delta_link: fragment(""),
                          ]]

  @drive_conflict_options [on_conflict: @drive_conflict_query,
                           conflict_target: :remote_id]

  @spec upsert_drive(map, [site: Site.t]) :: {:ok, Drive.t} | {:error, Ecto.Changeset.t}
  def upsert_drive(attrs, [site: site]) do
    Drive.changeset(attrs, site: site)
    |> Repo.insert(@drive_conflict_options)
  end

  @spec update_drive(Drive.t, [delta_link: String.t]) :: {:ok, Drive.t} | {:error, Ecto.Changeset.t}
  def update_drive(drive, [delta_link: delta_link]) do
    drive
    |> Drive.update_delta_link_changeset(delta_link)
    |> Repo.update()
  end

  @user_conflict_query from u in User,
                         update: [set: [
                           name: fragment("EXCLUDED.name")
                         ]]

  @user_conflict_options [on_conflict: @user_conflict_query,
                          conflict_target: :email]

  defp upsert_user(multi, user_info) do
    changeset = User.changeset(user_info)
    Multi.insert(multi, :user, changeset, @user_conflict_options)
  end

  @service_conflict_query from s in Service,
                            update: [set: [
                              title: fragment("EXCLUDED.title")
                            ]]

  @service_conflict_options [on_conflict: @service_conflict_query,
                             conflict_target: :resource_id]

  @spec upsert_services([map]) :: {:ok, [Service.t]} | {:error, any}
  defp upsert_services(infos) do
    case Multi.new |> upsert_services(infos) |> Repo.transaction() do
      {:ok, changes} ->
        services = for {{:service, _}, service} <- changes, do: service
        {:ok, services}
      error ->
        error
    end
  end

  @spec upsert_services(Multi.t, [map]) :: Multi.t
  defp upsert_services(multi, [info | infos]) do
    changeset = Service.changeset(info)
    {_, resource_id} = fetch_field(changeset, :resource_id)
    name = {:service, resource_id}

    multi
    |> Multi.insert(name, changeset, @service_conflict_options)
    |> upsert_services(infos)
  end

  defp upsert_services(multi, []), do: multi

  @spec upsert_authorizations(%{services: [Service.t], user: User.t}, [map]) :: {:ok, [Authorization.t]} | {:error, any}
  defp upsert_authorizations(%{services: services, user: user}, infos) do
    case Multi.new |> upsert_authorizations(user, services, infos) |> Repo.transaction() do
      {:ok, changes} ->
        auths = for {{:authorization, _}, authorization} <- changes, do: authorization
        {:ok, auths}
      error ->
        error
    end
  end

  @authorization_conflict_query from a in Authorization,
                                  update: [set: [
                                    access_token: fragment("EXCLUDED.access_token"),
                                    refresh_token: fragment("EXCLUDED.refresh_token")
                                  ]]

  @authorization_conflict_options [on_conflict: @authorization_conflict_query,
                                   conflict_target: [:user_id, :service_id]]

  @spec upsert_authorizations(Multi.t, User.t, [Service.t], [map]) :: Multi.t
  defp upsert_authorizations(multi, user, services, [info | infos]) do
    case Enum.find(services, &(&1.resource_id == Map.get(info, :resource_id))) do
      nil ->
        Multi.error(multi,
                    {:authorization, SecureRandom.hex()},
                    :missing_service_for_authorization_resource_id)
      service ->
        name = {:authorization, service.resource_id}

        changeset = Authorization.changeset(info, user: user, service: service)

        multi
        |> Multi.insert(name, changeset, @authorization_conflict_options)
        |> upsert_authorizations(user, services, infos)
    end
  end

  defp upsert_authorizations(multi, _, _, []), do: multi

  @spec build_upsert_user_and_services(map, [map], [map]) :: Multi.t
  defp build_upsert_user_and_services(user_info, services_info, authorizations_info) do
    Multi.new
    |> upsert_user(user_info)
    |> Multi.run(:services, fn _ -> upsert_services(services_info) end)
    |> Multi.run(:authorizations, &(upsert_authorizations(&1, authorizations_info)))
  end

  @spec upsert_remote_item(map, [drive: Drive.t, feed: Feed.t, parents: parents]) :: {:ok, any, parents} | {:error, any} | {:error, any, any, any}
  def upsert_remote_item(info, [drive: drive, feed: feed, parents: parents]) do
    attrs = Item.parse_remote(info)

    res = Multi.new
          |> insert_user_for_item_if_not_exists(attrs)
          |> upsert_item_with_parent(attrs, drive: drive, parents: parents)
          |> upsert_item_event(feed: feed)
          |> Repo.transaction()

    case res do
      {:ok, %{item: item} = result} ->
        {:ok, result, Map.put(parents, item.remote_id, item)}
      error ->
        error
    end
  end

  @other_user_conflict_query from u in User,
                               update: [set: [
                                 name: fragment("EXCLUDED.name")
                               ]]

  @other_user_conflict_options [on_conflict: @other_user_conflict_query,
                                conflict_target: :email,
                                returning: true]

  @spec insert_user_for_item_if_not_exists(Multi.t, map) :: Multi.t
  defp insert_user_for_item_if_not_exists(multi, %{user: nil}),
    do: multi

  defp insert_user_for_item_if_not_exists(multi, %{user: user_attrs}) do
    Multi.run(multi, :user, fn _ ->
      User.changeset(user_attrs)
      |> Repo.insert(@other_user_conflict_options)
    end)
  end

  @spec upsert_item_with_parent(Multi.t, map, [drive: Drive.t, parents: parents]) :: Multi.t
  defp upsert_item_with_parent(multi, %{parent_remote_id: nil} = item_attrs, [drive: drive, parents: _parents]) do
    multi
    |> upsert_item(item_attrs, drive: drive)
  end

  defp upsert_item_with_parent(multi, item_attrs, [drive: drive, parents: parents]) do
    case Map.fetch(parents, item_attrs.parent_remote_id) do
      {:ok, parent} ->
        multi
        |> upsert_item(item_attrs, drive: drive, parent: parent)
      :error ->
        Logger.error "cannot find parent for #{inspect({:item, item_attrs})}"
        multi
        |> Multi.error(:item, :cannot_find_parent_for_item)
    end
  end

  @spec item_changeset(map, [drive: Drive.t, parent: Item.t] | [drive: Drive.t]) :: Ecto.Changeset.t
  defp item_changeset(attrs, [drive: drive]),
    do: Item.changeset(attrs, drive: drive)

  defp item_changeset(attrs, [drive: drive, parent: parent]),
    do: Item.changeset(attrs, drive: drive, parent: parent)

  @item_conflict_query from i in Item,
                         update: [set: [
                           name: fragment("EXCLUDED.name"),
                           type: fragment("EXCLUDED.type"),
                           last_modified_at: fragment("EXCLUDED.last_modified_at"),
                           size: fragment("EXCLUDED.size"),
                           url: fragment("EXCLUDED.url"),
                           parent_id: fragment("EXCLUDED.parent_id")
                         ]]

  @item_conflict_options [on_conflict: @item_conflict_query,
                          conflict_target: [:remote_id, :drive_id],
                          returning: true]

  @spec upsert_item(Multi.t, map, [drive: Drive.t, parent: Item.t] | [drive: Drive.t]) :: Multi.t
  defp upsert_item(multi, attrs, opts) do
    Multi.run(multi, :item, fn _ ->
      item_changeset(attrs, opts)
      |> Repo.insert(@item_conflict_options)
    end)
  end

  @spec upsert_item_event(Multi.t, [feed: Feed.t]) :: Multi.t
  defp upsert_item_event(multi, [feed: feed]) do
    Multi.run(multi, :event, &upsert_item_event_multi_body(&1, feed))
  end

  defp upsert_item_event_multi_body(%{item: item, user: user}, %Feed{} = feed) do
    if Events.should_emit_event?(item, user) do
      Events.prepare_item_create_event(item, user)
      |> Keyword.merge([feed: feed])
      |> Feeds.upsert_event()
    else
      {:ok, nil}
    end
  end
  defp upsert_item_event_multi_body(_, _), do: {:ok, nil}

  @spec discover_parents([map], [drive: Drive.t]) :: parents
  def discover_parents([], _), do: %{}

  def discover_parents(infos, [drive: %{id: drive_id}]) do
    parent_ids = infos
                 |> Enum.map(&Item.assoc_remote_parent_remote_id/1)
                 |> Enum.drop_while(&is_nil/1)

    query = from i in Item,
              where: i.remote_id in ^parent_ids,
              where: i.drive_id == ^drive_id

    items = query |> Repo.all()

    for item <- items, into: %{}, do: {item.remote_id, item}
  end

  @spec upsert_or_delete_remote_items([map], [drive: Drive.t, feed: Feed.t]) :: transaction_result
  def upsert_or_delete_remote_items(infos, [drive: drive, feed: feed]) do
    deletes = for info <- infos, Map.has_key?(info, "deleted"), do: info
    upserts = infos -- deletes
    parents = discover_parents(upserts, drive: drive)

    delete_remote_items(deletes, drive: drive, feed: feed)
    upsert_remote_items(upserts, drive: drive, feed: feed, parents: parents)
  end

  @spec delete_remote_items([map], [drive: Drive.t, feed: Feed.t]) :: {integer, nil | [term]}
  defp delete_remote_items([], _), do: {0, []}

  defp delete_remote_items(infos, [drive: %{id: drive_id}, feed: _feed]) do
    now = DateTime.utc_now()
    remote_ids = for info <- infos, Map.has_key?(info, "id"), do: info["id"]
    query = from i in Item,
              where: i.remote_id in ^remote_ids,
              where: i.drive_id == ^drive_id,
              update: [set: [deleted_at: ^now]]

    Repo.update_all(query, [])
  end

  @spec upsert_remote_items([map], [drive: Drive.t, feed: Feed.t, parents: parents]) :: :ok | {:error, any}
  defp upsert_remote_items([info | infos], [drive: drive, feed: feed, parents: parents]) do
    case upsert_remote_item(info, drive: drive, feed: feed, parents: parents) do
      {:ok, _, parents} ->
        upsert_remote_items(infos, drive: drive, feed: feed, parents: parents) # NOTE: recurse
      {:error, _} = error ->
        error
      {:error, name, msg, info} ->
        {:error, {name, msg, info}}
    end
  end

  defp upsert_remote_items([], _), do: :ok # NOTE: done
end


Use heuristic for compiler errors (#4778)

Previously when all files being compiled get stuck waiting on
something else, we would release those files in random order.

This commit changes this to release files in groups based on
the module they are waiting on. We pick the module being
depended on with less edges, as it is mostly likely being the
cause of conflict. This may not always be true though: for example,
if there is a macro injecting code into multiple modules and such
code becomes faulty, now multiple modules are waiting on
this same module.

However, we have to pick one or the other, and we
decided to pick the one with fewer dependencies.

defmodule Kernel.ParallelCompiler do
  @moduledoc """
  A module responsible for compiling files in parallel.
  """

  @doc """
  Compiles the given files.

  Those files are compiled in parallel and can automatically
  detect dependencies between them. Once a dependency is found,
  the current file stops being compiled until the dependency is
  resolved.

  If there is an error during compilation or if `warnings_as_errors`
  is set to `true` and there is a warning, this function will fail
  with an exception.

  This function accepts the following options:

    * `:each_file` - for each file compiled, invokes the callback passing the
      file

    * `:each_long_compilation` - for each file that takes more than a given
      timeout (see the `:long_compilation_threshold` option) to compile, invoke
      this callback passing the file as its argument

    * `:long_compilation_threshold` - the timeout (in seconds) after the
      `:each_long_compilation` callback is invoked; defaults to `5`

    * `:each_module` - for each module compiled, invokes the callback passing
      the file, module and the module bytecode

    * `:dest` - the destination directory for the beam files. When using `files/2`,
      this information is only used to properly annotate the beam files before
      they are loaded into memory. If you want a file to actually be written to
      `dest`, use `files_to_path/3` instead.

  Returns the modules generated by each compiled file.
  """
  def files(files, options \\ [])

  def files(files, options) when is_list(options) do
    spawn_compilers(files, nil, options)
  end

  @doc """
  Compiles the given files to the given path.
  Read `files/2` for more information.
  """
  def files_to_path(files, path, options \\ [])

  def files_to_path(files, path, options) when is_binary(path) and is_list(options) do
    spawn_compilers(files, path, options)
  end

  defp spawn_compilers(files, path, options) do
    true = Code.ensure_loaded?(Kernel.ErrorHandler)
    compiler_pid = self()
    :elixir_code_server.cast({:reset_warnings, compiler_pid})
    schedulers = max(:erlang.system_info(:schedulers_online), 2)

    result = spawn_compilers(%{
      entries: files,
      original: files,
      output: path,
      options: options,
      waiting: [],
      queued: [],
      schedulers: schedulers,
      result: [],
    })

    # In case --warning-as-errors is enabled and there was a warning,
    # compilation status will be set to error.
    case :elixir_code_server.call({:compilation_status, compiler_pid}) do
      :ok ->
        result
      :error ->
        IO.puts :stderr, "Compilation failed due to warnings while using the --warnings-as-errors option"
        exit({:shutdown, 1})
    end
  end

  # We already have n=schedulers currently running, don't spawn new ones
  defp spawn_compilers(%{queued: queued, waiting: waiting, schedulers: schedulers} = state)
      when length(queued) - length(waiting) >= schedulers do
    wait_for_messages(state)
  end

  # Release waiting processes
  defp spawn_compilers(%{entries: [{h, kind} | t], waiting: waiting} = state) do
    waiting =
      case List.keytake(waiting, h, 1) do
        {{_kind, ^h, ref, _module, _defining}, waiting} ->
          send h, {ref, kind}
          waiting
        nil ->
          waiting
      end
    spawn_compilers(%{state | entries: t, waiting: waiting})
  end

  defp spawn_compilers(%{entries: [h | t], queued: queued, output: output, options: options} = state) do
    parent = self()

    {pid, ref} =
      :erlang.spawn_monitor fn ->
        # Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
        :erlang.put(:elixir_compiler_pid, parent)
        :erlang.process_flag(:error_handler, Kernel.ErrorHandler)

        exit(try do
          _ = if output do
            :elixir_compiler.file_to_path(h, output)
          else
            :elixir_compiler.file(h, Keyword.get(options, :dest))
          end
          {:shutdown, h}
        catch
          kind, reason ->
            {:failure, kind, reason, System.stacktrace}
        end)
      end

    timeout = Keyword.get(options, :long_compilation_threshold, 5) * 1_000
    timer_ref = Process.send_after(self(), {:timed_out, pid}, timeout)

    new_queued = [{pid, ref, h, timer_ref} | queued]
    spawn_compilers(%{state | entries: t, queued: new_queued})
  end

  # No more files, nothing waiting, queue is empty, we are done
  defp spawn_compilers(%{entries: [], waiting: [], queued: [], result: result}) do
    for {:module, mod} <- result, do: mod
  end

  # Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
  defp spawn_compilers(%{entries: [], waiting: waiting, queued: queued} = state) when length(waiting) == length(queued) do
    entries = for {pid, _, _, _} <- queued,
                  on = waiting_on_without_definition(waiting, pid),
                  do: {on, {pid, :not_found}}

    entries =
      entries
      |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
      |> Enum.sort_by(&length(elem(&1, 1)))
      |> Enum.at(0, {__MODULE__, []})
      |> elem(1)

    case entries do
      [] -> handle_deadlock(waiting, queued)
      _  -> spawn_compilers(%{state | entries: entries})
    end
  end

  # No more files, but queue and waiting are not full or do not match
  defp spawn_compilers(%{entries: []} = state) do
    wait_for_messages(state)
  end

  defp waiting_on_without_definition(waiting, pid) do
    {_kind, ^pid, _, on, _defining} = List.keyfind(waiting, pid, 1)
    if Enum.any?(waiting, fn {_, _, _, _, defining} -> on in defining end) do
      nil
    else
      on
    end
  end

  # Wait for messages from child processes
  defp wait_for_messages(state) do
    %{entries: entries, options: options, waiting: waiting, queued: queued, result: result} = state

    receive do
      {:struct_available, module} ->
        available = for {:struct, pid, _, waiting_module, _defining} <- waiting,
                        module == waiting_module,
                        do: {pid, :found}

        spawn_compilers(%{state | entries: available ++ entries, result: [{:struct, module} | result]})

      {:module_available, child, ref, file, module, binary} ->
        if callback = Keyword.get(options, :each_module) do
          callback.(file, module, binary)
        end

        # Release the module loader which is waiting for an ack
        send child, {ref, :ack}

        available = for {:module, pid, _, waiting_module, _defining} <- waiting,
                        module == waiting_module,
                        do: {pid, :found}

        cancel_waiting_timer(queued, child)

        spawn_compilers(%{state | entries: available ++ entries, result: [{:module, module} | result]})

      {:waiting, kind, child, ref, on, defining} ->
        defined = fn {k, m} -> on == m and k == kind end

        # Oops, we already got it, do not put it on waiting.
        waiting =
          if :lists.any(defined, result) do
            send child, {ref, :found}
            waiting
          else
            [{kind, child, ref, on, defining} | waiting]
          end

        spawn_compilers(%{state | waiting: waiting})

      {:timed_out, child} ->
        if callback = Keyword.get(options, :each_long_compilation) do
          {^child, _, file, _} = List.keyfind(queued, child, 0)
          callback.(file)
        end
        spawn_compilers(state)

      {:DOWN, _down_ref, :process, down_pid, {:shutdown, file}} ->
        if callback = Keyword.get(options, :each_file) do
          callback.(file)
        end

        cancel_waiting_timer(queued, down_pid)

        # Sometimes we may have spurious entries in the waiting
        # list because someone invoked try/rescue UndefinedFunctionError
        new_entries = List.delete(entries, down_pid)
        new_queued  = List.keydelete(queued, down_pid, 0)
        new_waiting = List.keydelete(waiting, down_pid, 1)
        spawn_compilers(%{state | entries: new_entries, waiting: new_waiting, queued: new_queued})

      {:DOWN, down_ref, :process, _down_pid, reason} ->
        handle_failure(down_ref, reason, queued)
        wait_for_messages(state)
    end
  end

  defp handle_deadlock(waiting, queued) do
    deadlock =
      for {pid, _, file, _} <- queued do
        {:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
        Process.exit(pid, :kill)

        {_kind, ^pid, _, on, _} = List.keyfind(waiting, pid, 1)
        error = CompileError.exception(description: "deadlocked waiting on module #{inspect on}",
                                       file: nil, line: nil)
        print_failure(file, {:failure, :error, error, stacktrace})

        {file, on}
      end

    IO.puts """

    Compilation failed because of a deadlock between files.
    The following files depended on the following modules:
    """

    max =
      deadlock
      |> Enum.map(& &1 |> elem(0) |> String.length)
      |> Enum.max

    for {file, mod} <- deadlock do
      IO.puts ["  ", String.pad_leading(file, max), " => " | inspect(mod)]
    end

    IO.puts ""
    exit({:shutdown, 1})
  end

  defp handle_failure(ref, reason, queued) do
    if file = find_failure(ref, queued) do
      print_failure(file, reason)
      for {pid, _, _, _} <- queued do
        Process.exit(pid, :kill)
      end
      exit({:shutdown, 1})
    end
  end

  defp find_failure(ref, queued) do
    case List.keyfind(queued, ref, 1) do
      {_child, ^ref, file, _timer_ref} -> file
      _ -> nil
    end
  end

  defp print_failure(_file, {:shutdown, _}) do
    :ok
  end

  defp print_failure(file, {:failure, kind, reason, stacktrace}) do
    IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
    IO.puts Exception.format(kind, reason, prune_stacktrace(stacktrace))
  end

  defp print_failure(file, reason) do
    IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
    IO.puts Exception.format(:exit, reason, [])
  end

  @elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
                     :elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
                     :elixir_def, :elixir_map, Kernel.ErrorHandler]

  defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
    prune_stacktrace(t)
  end

  defp prune_stacktrace([h | t]) do
    [h | prune_stacktrace(t)]
  end

  defp prune_stacktrace([]) do
    []
  end

  defp cancel_waiting_timer(queued, child_pid) do
    case List.keyfind(queued, child_pid, 0) do
      {^child_pid, _ref, _file, timer_ref} ->
        Process.cancel_timer(timer_ref)
        # Let's flush the message in case it arrived before we canceled the
        # timeout.
        receive do
          {:timed_out, ^child_pid} -> :ok
        after
          0 -> :ok
        end
      nil ->
        :ok
    end
  end
end

defmodule Kernel.ParallelCompiler do
  @moduledoc """
  A module responsible for compiling files in parallel.
  """

  @doc """
  Compiles the given files.

  Those files are compiled in parallel and can automatically
  detect dependencies between them. Once a dependency is found,
  the current file stops being compiled until the dependency is
  resolved.

  If there is an error during compilation or if `warnings_as_errors`
  is set to `true` and there is a warning, this function will fail
  with an exception.

  This function accepts the following options:

    * `:each_file` - for each file compiled, invokes the callback passing the
      file

    * `:each_long_compilation` - for each file that takes more than a given
      timeout (see the `:long_compilation_threshold` option) to compile, invoke
      this callback passing the file as its argument

    * `:long_compilation_threshold` - the timeout (in seconds) after the
      `:each_long_compilation` callback is invoked; defaults to `5`

    * `:each_module` - for each module compiled, invokes the callback passing
      the file, module and the module bytecode

    * `:dest` - the destination directory for the beam files. When using `files/2`,
      this information is only used to properly annotate the beam files before
      they are loaded into memory. If you want a file to actually be written to
      `dest`, use `files_to_path/3` instead.

  Returns the modules generated by each compiled file.
  """
  def files(files, options \\ [])

  def files(files, options) when is_list(options) do
    spawn_compilers(files, nil, options)
  end

  @doc """
  Compiles the given files to the given path.
  Read `files/2` for more information.
  """
  def files_to_path(files, path, options \\ [])

  def files_to_path(files, path, options) when is_binary(path) and is_list(options) do
    spawn_compilers(files, path, options)
  end

  defp spawn_compilers(files, path, options) do
    true = Code.ensure_loaded?(Kernel.ErrorHandler)
    compiler_pid = self()
    :elixir_code_server.cast({:reset_warnings, compiler_pid})
    schedulers = max(:erlang.system_info(:schedulers_online), 2)

    result = spawn_compilers(%{
      entries: files,
      original: files,
      output: path,
      options: options,
      waiting: [],
      queued: [],
      schedulers: schedulers,
      result: [],
    })

    # In case --warning-as-errors is enabled and there was a warning,
    # compilation status will be set to error.
    case :elixir_code_server.call({:compilation_status, compiler_pid}) do
      :ok ->
        result
      :error ->
        IO.puts :stderr, "Compilation failed due to warnings while using the --warnings-as-errors option"
        exit({:shutdown, 1})
    end
  end

  # We already have n=schedulers currently running, don't spawn new ones
  defp spawn_compilers(%{queued: queued, waiting: waiting, schedulers: schedulers} = state)
      when length(queued) - length(waiting) >= schedulers do
    wait_for_messages(state)
  end

  # Release waiting processes
  defp spawn_compilers(%{entries: [{h, kind} | t], waiting: waiting} = state) do
    waiting =
      case List.keytake(waiting, h, 1) do
        {{_kind, ^h, ref, _module, _defining}, waiting} ->
          send h, {ref, kind}
          waiting
        nil ->
          waiting
      end
    spawn_compilers(%{state | entries: t, waiting: waiting})
  end

  defp spawn_compilers(%{entries: [h | t], queued: queued, output: output, options: options} = state) do
    parent = self()

    {pid, ref} =
      :erlang.spawn_monitor fn ->
        # Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
        :erlang.put(:elixir_compiler_pid, parent)
        :erlang.process_flag(:error_handler, Kernel.ErrorHandler)

        exit(try do
          _ = if output do
            :elixir_compiler.file_to_path(h, output)
          else
            :elixir_compiler.file(h, Keyword.get(options, :dest))
          end
          {:shutdown, h}
        catch
          kind, reason ->
            {:failure, kind, reason, System.stacktrace}
        end)
      end

    timeout = Keyword.get(options, :long_compilation_threshold, 5) * 1_000
    timer_ref = Process.send_after(self(), {:timed_out, pid}, timeout)

    new_queued = [{pid, ref, h, timer_ref} | queued]
    spawn_compilers(%{state | entries: t, queued: new_queued})
  end

  # No more files, nothing waiting, queue is empty, we are done
  defp spawn_compilers(%{entries: [], waiting: [], queued: [], result: result}) do
    for {:module, mod} <- result, do: mod
  end

  # Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
  defp spawn_compilers(%{entries: [], waiting: waiting, queued: queued} = state) when length(waiting) == length(queued) do
    entries = for {pid, _, _, _} <- queued,
                  not waiting_on_is_being_defined?(waiting, pid),
                  do: {pid, :not_found}

    case entries do
      [] -> handle_deadlock(waiting, queued)
      _  -> spawn_compilers(%{state | entries: entries})
    end
  end

  # No more files, but queue and waiting are not full or do not match
  defp spawn_compilers(%{entries: []} = state) do
    wait_for_messages(state)
  end

  defp waiting_on_is_being_defined?(waiting, pid) do
    {_kind, ^pid, _, on, _defining} = List.keyfind(waiting, pid, 1)
    Enum.any?(waiting, fn {_, _, _, _, defining} -> on in defining end)
  end

  # Wait for messages from child processes
  defp wait_for_messages(state) do
    %{entries: entries, options: options, waiting: waiting, queued: queued, result: result} = state

    receive do
      {:struct_available, module} ->
        available = for {:struct, pid, _, waiting_module, _defining} <- waiting,
                        module == waiting_module,
                        do: {pid, :found}

        spawn_compilers(%{state | entries: available ++ entries, result: [{:struct, module} | result]})

      {:module_available, child, ref, file, module, binary} ->
        if callback = Keyword.get(options, :each_module) do
          callback.(file, module, binary)
        end

        # Release the module loader which is waiting for an ack
        send child, {ref, :ack}

        available = for {:module, pid, _, waiting_module, _defining} <- waiting,
                        module == waiting_module,
                        do: {pid, :found}

        cancel_waiting_timer(queued, child)

        spawn_compilers(%{state | entries: available ++ entries, result: [{:module, module} | result]})

      {:waiting, kind, child, ref, on, defining} ->
        defined = fn {k, m} -> on == m and k == kind end

        # Oops, we already got it, do not put it on waiting.
        waiting =
          if :lists.any(defined, result) do
            send child, {ref, :found}
            waiting
          else
            [{kind, child, ref, on, defining} | waiting]
          end

        spawn_compilers(%{state | waiting: waiting})

      {:timed_out, child} ->
        if callback = Keyword.get(options, :each_long_compilation) do
          {^child, _, file, _} = List.keyfind(queued, child, 0)
          callback.(file)
        end
        spawn_compilers(state)

      {:DOWN, _down_ref, :process, down_pid, {:shutdown, file}} ->
        if callback = Keyword.get(options, :each_file) do
          callback.(file)
        end

        cancel_waiting_timer(queued, down_pid)

        # Sometimes we may have spurious entries in the waiting
        # list because someone invoked try/rescue UndefinedFunctionError
        new_entries = List.delete(entries, down_pid)
        new_queued  = List.keydelete(queued, down_pid, 0)
        new_waiting = List.keydelete(waiting, down_pid, 1)
        spawn_compilers(%{state | entries: new_entries, waiting: new_waiting, queued: new_queued})

      {:DOWN, down_ref, :process, _down_pid, reason} ->
        handle_failure(down_ref, reason, queued)
        wait_for_messages(state)
    end
  end

  defp handle_deadlock(waiting, queued) do
    deadlock =
      for {pid, _, file, _} <- queued do
        {:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
        Process.exit(pid, :kill)

        {_kind, ^pid, _, on, _} = List.keyfind(waiting, pid, 1)
        error = CompileError.exception(description: "deadlocked waiting on module #{inspect on}",
                                       file: nil, line: nil)
        print_failure(file, {:failure, :error, error, stacktrace})

        {file, on}
      end

    IO.puts """

    Compilation failed because of a deadlock between files.
    The following files depended on the following modules:
    """

    max =
      deadlock
      |> Enum.map(& &1 |> elem(0) |> String.length)
      |> Enum.max

    for {file, mod} <- deadlock do
      IO.puts ["  ", String.pad_leading(file, max), " => " | inspect(mod)]
    end

    IO.puts ""
    exit({:shutdown, 1})
  end

  defp handle_failure(ref, reason, queued) do
    if file = find_failure(ref, queued) do
      print_failure(file, reason)
      for {pid, _, _, _} <- queued do
        Process.exit(pid, :kill)
      end
      exit({:shutdown, 1})
    end
  end

  defp find_failure(ref, queued) do
    case List.keyfind(queued, ref, 1) do
      {_child, ^ref, file, _timer_ref} -> file
      _ -> nil
    end
  end

  defp print_failure(_file, {:shutdown, _}) do
    :ok
  end

  defp print_failure(file, {:failure, kind, reason, stacktrace}) do
    IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
    IO.puts Exception.format(kind, reason, prune_stacktrace(stacktrace))
  end

  defp print_failure(file, reason) do
    IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
    IO.puts Exception.format(:exit, reason, [])
  end

  @elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
                     :elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
                     :elixir_def, :elixir_map, Kernel.ErrorHandler]

  defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
    prune_stacktrace(t)
  end

  defp prune_stacktrace([h | t]) do
    [h | prune_stacktrace(t)]
  end

  defp prune_stacktrace([]) do
    []
  end

  defp cancel_waiting_timer(queued, child_pid) do
    case List.keyfind(queued, child_pid, 0) do
      {^child_pid, _ref, _file, timer_ref} ->
        Process.cancel_timer(timer_ref)
        # Let's flush the message in case it arrived before we canceled the
        # timeout.
        receive do
          {:timed_out, ^child_pid} -> :ok
        after
          0 -> :ok
        end
      nil ->
        :ok
    end
  end
end


don't need to match true/false, just pass the value through

defmodule Ello.V2.ClientProperties do
  use Plug.Builder

  plug :android
  plug :ios_version_from_header
  plug :ios_version_from_ua
  plug :ios
  plug :webapp
  plug :nudity
  plug :nsfw

  defp android(conn, _) do
    with [ua | _]  <- get_req_header(conn, "user_agent"),
         true      <- Regex.match?(~r/Ello Android/i, ua) do
      assign(conn, :android, true)
    else
      _ -> assign(conn, :android, false)
    end
  end

  defp ios_version_from_header(%{assigns: %{android: true}} = conn, _),
    do: conn
  defp ios_version_from_header(conn, _) do
    case get_req_header(conn, "x-ios-build-number") do
      [build | _] -> assign(conn, :ios_version, String.to_integer(build))
      _           -> conn
    end
  end

  defp ios_version_from_ua(%{assigns: %{ios_version: v}} = conn, _)
    when is_integer(v),
    do: conn
  defp ios_version_from_ua(%{assigns: %{android: true}} = conn, _),
    do: conn
  defp ios_version_from_ua(conn, _) do
    with [ua | _]   <- get_req_header(conn, "user_agent"),
         [_, build] <- Regex.run(~r[^Ello\/(\d{4,6}).*Darwin.*], ua) do
      assign(conn, :ios_version, String.to_integer(build))
    else
      _ -> conn
    end
  end

  defp ios(%{assigns: %{ios_version: v}} = conn, _) when is_integer(v),
    do: assign(conn, :ios, true)
  defp ios(conn, _),
    do: assign(conn, :ios, false)

  defp webapp(%{assigns: %{android: false, ios: false}} = conn, _),
    do: assign(conn, :webapp, true)
  defp webapp(conn, _),
    do: assign(conn, :webapp, false)

  defp nudity(%{assigns: %{user: %{settings: %{views_adult_content: true}}}} = conn, _),
    do: assign(conn, :allow_nudity, true)
  defp nudity(%{assigns: %{webapp: true}} = conn, _),
    do: assign(conn, :allow_nudity, true)
  defp nudity(conn, _),
    do: assign(conn, :allow_nudity, false)

  defp nsfw(%{assigns: %{user: %{settings: %{views_adult_content: allow_nsfw}}}} = conn, _),
    do: assign(conn, :allow_nsfw, allow_nsfw)
  defp nsfw(%{assigns: %{webapp: true}} = conn, _),
    do: assign(conn, :allow_nsfw, true)
  defp nsfw(conn, _),
    do: assign(conn, :allow_nsfw, false)
end

defmodule Ello.V2.ClientProperties do
  use Plug.Builder

  plug :android
  plug :ios_version_from_header
  plug :ios_version_from_ua
  plug :ios
  plug :webapp
  plug :nudity
  plug :nsfw

  defp android(conn, _) do
    with [ua | _]  <- get_req_header(conn, "user_agent"),
         true      <- Regex.match?(~r/Ello Android/i, ua) do
      assign(conn, :android, true)
    else
      _ -> assign(conn, :android, false)
    end
  end

  defp ios_version_from_header(%{assigns: %{android: true}} = conn, _),
    do: conn
  defp ios_version_from_header(conn, _) do
    case get_req_header(conn, "x-ios-build-number") do
      [build | _] -> assign(conn, :ios_version, String.to_integer(build))
      _           -> conn
    end
  end

  defp ios_version_from_ua(%{assigns: %{ios_version: v}} = conn, _)
    when is_integer(v),
    do: conn
  defp ios_version_from_ua(%{assigns: %{android: true}} = conn, _),
    do: conn
  defp ios_version_from_ua(conn, _) do
    with [ua | _]   <- get_req_header(conn, "user_agent"),
         [_, build] <- Regex.run(~r[^Ello\/(\d{4,6}).*Darwin.*], ua) do
      assign(conn, :ios_version, String.to_integer(build))
    else
      _ -> conn
    end
  end

  defp ios(%{assigns: %{ios_version: v}} = conn, _) when is_integer(v),
    do: assign(conn, :ios, true)
  defp ios(conn, _),
    do: assign(conn, :ios, false)

  defp webapp(%{assigns: %{android: false, ios: false}} = conn, _),
    do: assign(conn, :webapp, true)
  defp webapp(conn, _),
    do: assign(conn, :webapp, false)

  defp nudity(%{assigns: %{user: %{settings: %{views_adult_content: true}}}} = conn, _),
    do: assign(conn, :allow_nudity, true)
  defp nudity(%{assigns: %{webapp: true}} = conn, _),
    do: assign(conn, :allow_nudity, true)
  defp nudity(conn, _),
    do: assign(conn, :allow_nudity, false)

  defp nsfw(%{assigns: %{user: %{settings: %{views_adult_content: true}}}} = conn, _),
    do: assign(conn, :allow_nsfw, true)
  defp nsfw(%{assigns: %{user: %{settings: %{views_adult_content: false}}}} = conn, _),
    do: assign(conn, :allow_nsfw, false)
  defp nsfw(%{assigns: %{webapp: true}} = conn, _),
    do: assign(conn, :allow_nsfw, true)
  defp nsfw(conn, _),
    do: assign(conn, :allow_nsfw, false)
end


add documentation

add documentation

defmodule Maven do
  alias Porcelain.Process, as: Proc

  @moduledoc """
  Run Maven.
  """

  defmodule Result do
    @moduledoc """
    Result of running Maven.
    """

    defstruct [:status, :exit, :filtered]

    @type t :: %__MODULE__{}
  end

  @mvn "mvn"

  @doc """
  Run maven with the given options `mvn_opts` and an optional filter function `filter`.
  """
  @spec mvn(list(String.t), (String.t -> list)) :: Maven.Result.t
  def mvn(mvn_opts, filter \\ &([&1])) do
    # it is important to :keep the result, otherwise
    # you cannot wait for it
    proc = Porcelain.spawn(@mvn, mvn_opts, out: :stream, result: :keep)
    filtered = proc.out
      |> Stream.map(&String.strip(&1))
      |> Stream.each(&IO.puts(&1))
      |> Stream.flat_map(&filter.(&1))
      |> Enum.into([])
    # await Maven to finish and transform the return code
    case Proc.await(proc) do
      {:ok, %Porcelain.Result{status: 0}} ->
        %Result{status: :ok, exit: nil, filtered: filtered}
      {:ok, %Porcelain.Result{status: exit}} ->
        %Result{status: :error, exit: exit, filtered: filtered}
      # crash otherwise
    end
  end

  @regex ~r/^\[INFO\] Installing (.*?\.jar) to .*?\.jar$/
  @doc """
  Predefined filter function for `mvn/2` to extract messages about installed jars.
  """
  def filter_find_install_jar(line) do
    match = @regex |> Regex.run(line)
    case match do
      nil -> []
      [_, jar] -> [jar]
    end
  end
end

defmodule Maven do
  alias Porcelain.Process, as: Proc

  @moduledoc """
  Run Maven.
  """

  defmodule Result do
    @moduledoc """
    Result of running Maven.
    """

    defstruct [:status, :exit, :filtered]

    @type t :: %__MODULE__{}
  end

  @mvn "mvn"

  @doc """
  """
  @spec mvn(list(String.t), (String.t -> list)) :: Maven.Result.t
  def mvn(mvn_opts, filter \\ &([&1])) do
    # it is important to :keep the result, otherwise
    # you cannot wait for it
    proc = Porcelain.spawn(@mvn, mvn_opts, out: :stream, result: :keep)
    filtered = proc.out
      |> Stream.map(&String.strip(&1))
      |> Stream.each(&IO.puts(&1))
      |> Stream.flat_map(&filter.(&1))
      |> Enum.into([])
    # await Maven to finish and transform the return code
    case Proc.await(proc) do
      {:ok, %Porcelain.Result{status: 0}} ->
        %Result{status: :ok, exit: nil, filtered: filtered}
      {:ok, %Porcelain.Result{status: exit}} ->
        %Result{status: :error, exit: exit, filtered: filtered}
      # crash otherwise
    end
  end

  @regex ~r/^\[INFO\] Installing (.*?\.jar) to .*?\.jar$/
  def filter_find_install_jar(line) do
    match = @regex |> Regex.run(line)
    case match do
      nil -> []
      [_, jar] -> [jar]
    end
  end
end


deletion strategy for alternate feeds and feeds

defmodule Pan.Feed do
  use Pan.Web, :model

  schema "feeds" do
    field :self_link_title, :string
    field :self_link_url, :string
    field :next_page_url, :string
    field :prev_page_url, :string
    field :first_page_url, :string
    field :last_page_url, :string
    field :hub_link_url, :string
    field :feed_generator, :string
    timestamps

    belongs_to :podcast, Pan.Podcast
    has_many :alternate_feeds, Pan.AlternateFeed, on_delete: :delete_all
  end

  @required_fields ~w(self_link_url )
  @optional_fields ~w(self_link_title next_page_url prev_page_url first_page_url last_page_url hub_link_url feed_generator)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ %{}) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:alternate_feeds)
  end
end

defmodule Pan.Feed do
  use Pan.Web, :model

  schema "feeds" do
    field :self_link_title, :string
    field :self_link_url, :string
    field :next_page_url, :string
    field :prev_page_url, :string
    field :first_page_url, :string
    field :last_page_url, :string
    field :hub_link_url, :string
    field :feed_generator, :string
    timestamps

    belongs_to :podcast, Pan.Podcast
    has_many :alternate_feeds, Pan.AlternateFeed
  end

  @required_fields ~w(self_link_url )
  @optional_fields ~w(self_link_title next_page_url prev_page_url first_page_url last_page_url hub_link_url feed_generator)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ %{}) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> cast_assoc(:alternate_feeds)
  end
end


Fixes: color is a string, not a symbol

Fixes: color is a string, not a symbol

defmodule Cellar.Wine do
  use Cellar.Web, :model

  schema "wines" do
    field :name, :string
    field :designation, :string
    field :vintage, :integer
    field :ready_to_drink, :string
    field :color, :string
    field :notes, :string
    # has_many :bottles, Cellar.Bottle

    timestamps
  end

  @required_fields ~w(name designation vintage ready_to_drink color)
  @optional_fields ~w(notes)
  @colors ~w(red white rose)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_inclusion(:color, @colors)
    |> validate_inclusion(:vintage, 1900..2050)
    |> validate_format(:ready_to_drink, ~r/^[0-9]{4}(-[0-9]{4})?$/) # '2016' or '2016-2018'
  end

  def search(query, search_input) do
    search_term = search_input
    |> String.split(" ")
    |> Enum.map(&("#{&1}*"))
    |> Enum.join(" ")

    from(w in query,
      where: fragment("MATCH(name, designation, color) AGAINST(? IN BOOLEAN MODE)", ^search_term),
      order_by: [desc: fragment("MATCH(name, designation, color) AGAINST(? IN BOOLEAN MODE)", ^search_term)])
  end
end

defmodule Cellar.Wine do
  use Cellar.Web, :model

  schema "wines" do
    field :name, :string
    field :designation, :string
    field :vintage, :integer
    field :ready_to_drink, :string
    field :color, :string
    field :notes, :string
    # has_many :bottles, Cellar.Bottle

    timestamps
  end

  @required_fields ~w(name designation vintage ready_to_drink color)
  @optional_fields ~w(notes)
  @colors ~w(red white rose)a

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_inclusion(:color, @colors)
    |> validate_inclusion(:vintage, 1900..2050)
    |> validate_format(:ready_to_drink, ~r/^[0-9]{4}(-[0-9]{4})?$/) # '2016' or '2016-2018'
  end

  def search(query, search_input) do
    search_term = search_input
    |> String.split(" ")
    |> Enum.map(&("#{&1}*"))
    |> Enum.join(" ")

    from(w in query,
      where: fragment("MATCH(name, designation, color) AGAINST(? IN BOOLEAN MODE)", ^search_term),
      order_by: [desc: fragment("MATCH(name, designation, color) AGAINST(? IN BOOLEAN MODE)", ^search_term)])
  end
end


testing format_results/1 and format_issues/1

defmodule Slax.GithubCommands.Test do
  use Slax.ModelCase, async: true
  alias Slax.Commands.GithubCommands
  import Mox

  describe "format_issues/1" do
    test "when there is an 'in progress' label" do
      assert GithubCommands.format_issues([
               %{
                 "title" => "title",
                 "updated_at" => "2019-04-18T14:08:35Z",
                 "labels" => [%{"name" => "in progress"}]
               }
             ]) =~ "Last Updated at"
    end

    test "when there is not an 'in progress' label" do
      assert GithubCommands.format_issues([
               %{
                 "title" => "title",
                 "updated_at" => "2019-04-18T14:08:35Z",
                 "labels" => [%{"name" => "back log"}]
               }
             ]) =~ ":snail: *Issues In Progress for 5/18* :snail: \n"
    end
  end

  describe "format_results/1" do
    setup :setup_no_errors

    def setup_no_errors(context) do
      {:ok,
       context
       |> Map.put(:starting_result, %{
         project_name: true,
         github_repo: true,
         slack_channel: true,
         errors: %{},
         success: %{
           project_name: "project_name",
           github_repo: "org_name/project_name",
           slack_channel: "slack_channel"
         }
       })}
    end

    test "when there are no errors for any steps", %{starting_result: starting_result} do
      result = GithubCommands.format_results(starting_result)

      assert result ==
               "Project Name: project_name\nGithub: org_name/project_name\nGithub Teams: \nSlack: slack_channel\nLintron: \nBoard Checker: \nReusable Stories: "
    end

    test "when there are errors for certain steps" do
      starting_result = %{
        project_name: true,
        github_repo: true,
        errors: %{
          slack_channel: "slack_channel error"
        },
        success: %{
          project_name: "project_name",
          github_repo: "org_name/project_name"
        }
      }

      result = GithubCommands.format_results(starting_result)

      assert result ==
               "Project Name: project_name\nGithub: org_name/project_name\nGithub Teams: \nSlack: slack_channel error\nLintron: \nBoard Checker: \nReusable Stories: "
    end
  end

  describe "parse_project_name/2" do
    def setup_starting_result(context) do
      {:ok,
       context
       |> Map.put(:starting_result, %{errors: %{}, success: %{}})}
    end

    setup :setup_starting_result

    test "invalid characters", %{starting_result: starting_result} do
      with result <- GithubCommands.parse_project_name(starting_result, "hi&") do
        assert Map.get(result, :errors) == %{project_name: "Invalid Project Name"}
        assert Map.get(result, :project_name) == nil
      end
    end

    test "too many characters", %{starting_result: starting_result} do
      with result <-
             GithubCommands.parse_project_name(
               starting_result,
               "thisshouldbemorethantwentytwocharacters"
             ) do
        assert Map.get(result, :errors) == %{project_name: "Invalid Project Name"}
        assert Map.get(result, :project_name) == nil
      end
    end

    test "valid_input", %{starting_result: starting_result} do
      with result <- GithubCommands.parse_project_name(starting_result, "valid_repo_name") do
        assert Map.get(result, :errors) == %{}
        assert Map.get(result, :success) == %{project_name: "Project Name Parsed"}
        assert Map.get(result, :project_name) == "valid_repo_name"
      end
    end
  end

  def setup_starting_result_with_name(context) do
    {:ok,
     context
     |> Map.put(:starting_result, %{errors: %{}, success: %{}})
     |> put_in([:starting_result, :success, :project_name], "Project Name Parsed")
     |> put_in([:starting_result, :project_name], "valid_project_name")}
  end

  def successful_tree_request(_, _, _) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
             "tree": [
               {
                 "path": "story_path1.md",
                 "sha": "test_sha",
                 "type": "blob"
               },
               {
                 "path": "story_path3.md",
                 "sha": "test_sha",
                 "type": "blob"
               }

             ]
           }>
     }}
  end

  def successful_blob_request(_, _, _) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
           "content": "LS0tCnRpdGxlOiB0ZXN0X3RpdGxlCi0tLQp0ZXN0IGNvbnRlbnQK"
         }>
     }}
  end

  def successful_issue_request(_, _, _, _) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
         "content": ["1"]
       }>
     }}
  end

  describe "create_reusable_stories/5" do
    setup [:setup_starting_result_with_name]

    test "when all requests are successful", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &successful_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:errors] == %{}
      assert result[:project_name] == "valid_project_name"
      assert result[:reusable_stories] == true
      assert result[:success][:reusable_stories] == "Reuseable Stories Created"
    end

    def failing_request(_, _, _) do
      {:ok,
       %HTTPoison.Response{
         status_code: 400,
         body: ~s<{
           "message": "you done goofed"
         }>
       }}
    end

    def failing_request(w, x, y, _), do: failing_request(w, x, y)

    test "when fetching the project tree fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &failing_request/3)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil
      assert result[:errors] == %{reusable_stories: "you done goofed"}
      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    test "when fetching a blob fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &failing_request/3)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
               reusable_stories:
                 "story_path1.md: you done goofed\nstory_path3.md: you done goofed"
             }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    test "when posting issues fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &successful_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &failing_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
               reusable_stories:
                 "story_path1.md: you done goofed\nstory_path3.md: you done goofed"
             }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    def invalid_blob_request(_, _, _) do
      {:ok,
       %HTTPoison.Response{
         status_code: 200,
         body: ~s<{
           "content": "invalidblob"
         }>
       }}
    end

    test "when decoding a blob fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &invalid_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
               reusable_stories:
                 "story_path1.md: Unable to parse content\nstory_path3.md: Unable to parse content"
             }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    def invalid_frontmatter_request(_, _, _) do
      {:ok,
       %HTTPoison.Response{
         status_code: 200,
         body: ~s<{
           "content": "LS0KaW52YWxpZCBmcm9udG1hdHRlcgpfCm9vcHMK"
         }>
       }}
    end

    test "when parsing issue frontmatter fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &invalid_frontmatter_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
               reusable_stories:
                 "story_path1.md: invalid_front_matter\nstory_path3.md: invalid_front_matter"
             }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end
  end
end

defmodule Slax.GithubCommands.Test do
  use Slax.ModelCase, async: true
  alias Slax.Commands.GithubCommands
  import Mox

  test "format_issues/1" do
    assert GithubCommands.format_issues([
             %{
               "title" => "title",
               "updated_at" => "2019-04-18T14:08:35Z",
               "labels" => [%{"name" => "in progress"}]
             }
           ]) =~ "Last Updated at"
  end

  describe "parse_project_name/2" do
    def setup_starting_result(context) do
      {:ok,
       context
       |> Map.put(:starting_result, %{errors: %{}, success: %{}})}
    end

    setup :setup_starting_result

    test "invalid characters", %{starting_result: starting_result} do
      with result <- GithubCommands.parse_project_name(starting_result, "hi&") do
        assert Map.get(result, :errors) == %{project_name: "Invalid Project Name"}
        assert Map.get(result, :project_name) == nil
      end
    end

    test "too many characters", %{starting_result: starting_result} do
      with result <-
             GithubCommands.parse_project_name(
               starting_result,
               "thisshouldbemorethantwentytwocharacters"
             ) do
        assert Map.get(result, :errors) == %{project_name: "Invalid Project Name"}
        assert Map.get(result, :project_name) == nil
      end
    end

    test "valid_input", %{starting_result: starting_result} do
      with result <- GithubCommands.parse_project_name(starting_result, "valid_repo_name") do
        assert Map.get(result, :errors) == %{}
        assert Map.get(result, :success) == %{project_name: "Project Name Parsed"}
        assert Map.get(result, :project_name) == "valid_repo_name"
      end
    end
  end

  def setup_starting_result_with_name(context) do
    {:ok,
     context
     |> Map.put(:starting_result, %{errors: %{}, success: %{}})
     |> put_in([:starting_result, :success, :project_name], "Project Name Parsed")
     |> put_in([:starting_result, :project_name], "valid_project_name")}
  end

  def setup_successful_tree_requests(context) do
    {:ok,
     context
     |> Map.put(:starting_result, %{errors: %{}, success: %{}})
     |> put_in([:starting_result, :success, :project_name], "Project Name Parsed")
     |> put_in([:starting_result, :project_name], "valid_project_name")}
  end

  def successful_tree_request(_, _, _) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
             "tree": [
               {
                 "path": "story_path1.md",
                 "sha": "test_sha",
                 "type": "blob"
               },
               {
                 "path": "story_path3.md",
                 "sha": "test_sha",
                 "type": "blob"
               }

             ]
           }>
     }}
  end

  def successful_blob_request(_, _, _) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
           "content": "LS0tCnRpdGxlOiB0ZXN0X3RpdGxlCi0tLQp0ZXN0IGNvbnRlbnQK"
         }>
     }}
  end

  def successful_issue_request(_,_,_,_) do
    {:ok,
     %HTTPoison.Response{
       status_code: 200,
       body: ~s<{
         "content": ["1"]
       }>
     }}
  end

  describe "create_reusable_stories/5" do
    setup [:setup_starting_result_with_name]

    test "when all requests are successful", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &successful_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:errors] == %{}
      assert result[:project_name] == "valid_project_name"
      assert result[:reusable_stories] == true
      assert result[:success][:reusable_stories] == "Reuseable Stories Created"
    end

    def failing_request(_,_,_) do
      {:ok,
       %HTTPoison.Response{
         status_code: 400,
         body: ~s<{
           "message": "you done goofed"
         }>
       }}
    end
    def failing_request(w,x,y,_), do: failing_request(w,x,y)

    test "when fetching the project tree fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &failing_request/3)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil
      assert result[:errors] == %{reusable_stories: "you done goofed"}
      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    test "when fetching a blob fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &failing_request/3)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
               reusable_stories:
                 "story_path1.md: you done goofed\nstory_path3.md: you done goofed"
             }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    test "when posting issues fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &successful_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &failing_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
        reusable_stories:
        "story_path1.md: you done goofed\nstory_path3.md: you done goofed"
      }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    def invalid_blob_request(_,_,_) do
      {:ok,
       %HTTPoison.Response{
         status_code: 200,
         body: ~s<{
           "content": "invalidblob"
         }>
       }}
    end

    test "when decoding a blob fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &invalid_blob_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
        reusable_stories:
        "story_path1.md: Unable to parse content\nstory_path3.md: Unable to parse content"
      }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end

    def invalid_frontmatter_request(_,_,_) do
      {:ok,
       %HTTPoison.Response{
         status_code: 200,
         body: ~s<{
           "content": "LS0KaW52YWxpZCBmcm9udG1hdHRlcgpfCm9vcHMK"
         }>
       }}
    end

    test "when parsing issue frontmatter fails", %{starting_result: starting_result} do
      expect(Slax.HttpMock, :get, 1, &successful_tree_request/3)
      expect(Slax.HttpMock, :get, 2, &invalid_frontmatter_request/3)
      expect(Slax.HttpMock, :post, 3, &successful_issue_request/4)

      result =
        GithubCommands.create_reusable_stories(
          starting_result,
          "access_token",
          "test_org",
          "story_repo",
          path1: "story_path1",
          path3: "story_path3"
        )

      assert result[:reusable_stories] == nil

      assert result[:errors] == %{
        reusable_stories:
        "story_path1.md: invalid_front_matter\nstory_path3.md: invalid_front_matter"
      }

      assert result[:project_name] == "valid_project_name"
      assert result[:success] == %{project_name: "Project Name Parsed"}
    end
  end
end


Don't blow alawy mnesia schema on start up

Default mnesia behaviour is not require a disc schema if tables all reside in
memory. Because all our test tables are in memory they shouldn't exist on start
up.

If creating a table fails it means it already exists and we would be clobbering
it.

:stopped = :mnesia.stop
:ok = :mnesia.delete_schema([node()])
:ok = :mnesia.create_schema([node()])
:ok = :mnesia.start()

{:atomic, :ok} = BattleSnake.GameForm.create_table(ram_copies: [node()])
{:atomic, :ok} = BattleSnake.World.create_table(ram_copies: [node()])

ExUnit.start
ExUnit.plural_rule("property", "properties")


return a list instead of a Set of services for Directory.nodes/1

defmodule Discovery.Directory do
  @moduledoc """
  A registered process that contains the state of known nodes and the services
  that they provide.
  """

  use GenServer
  @name Discovery.Directory

  def start_link do
    Agent.start_link(fn -> %{nodes: %{}, services: %{}} end, name: @name)
  end

  @doc """
  Add a node and the service it provides to the directory.
  """
  @spec add(atom, binary) :: :ok
  def add(node, service) when is_atom(node) and is_binary(service) do
    Agent.update(@name, fn(%{nodes: nodes, services: services} = state) ->
      case Dict.fetch(services, service) do
        :error ->
          new_services = Dict.put(services, service, HashSet.new |> Set.put(node))
        {:ok, nodes} ->
          new_services = Dict.put(services, service, Set.put(nodes, node))
      end

      case Dict.fetch(nodes, node) do
        :error ->
          new_nodes = Dict.put(nodes, node, HashSet.new |> Set.put(service))
        {:ok, node_services} ->
          new_nodes = Dict.put(nodes, node, Set.put(node_services, service))
      end

      %{state | nodes: new_nodes, services: new_services}
    end)
  end

  @doc """
  Clear all registered nodes from the directory.
  """
  @spec clear :: :ok
  def clear do
    Agent.update(@name, fn(_) ->
      %{nodes: %{}, services: %{}}
    end)
  end

  @doc """
  Drop a node from the directory.
  """
  @spec drop(atom) :: :ok
  def drop(node) when is_atom(node) do
    Agent.update(@name, fn(%{nodes: nodes, services: services} = state) ->
      case Dict.pop(nodes, node) do
        {nil, new_nodes} ->
          new_nodes    = new_nodes
          new_services = services
        {_, new_nodes} ->
          new_nodes    = new_nodes
          new_services = Enum.reduce(services, %{}, fn({key, value}, acc) ->
            new_set = Set.delete(value, node)
            case Enum.empty?(new_set) do
              true ->
                acc
              false ->
                Map.put(acc, key, new_set)
            end
          end)
      end

      %{state | nodes: new_nodes, services: new_services}
    end)
  end

  @doc """
  Checks if node exists within the Directory.
  """
  @spec has_node?(atom) :: boolean
  def has_node?(node) when is_atom(node) do
    Agent.get(@name, fn(%{nodes: nodes}) ->
      Map.has_key?(nodes, node)
    end)
  end

  @doc """
  List all nodes and the services they provide.
  """
  @spec nodes :: Set.t
  def nodes do
    Agent.get(@name, fn(%{nodes: nodes}) -> nodes end)
  end

  @doc """
  List all nodes which provide the given service.
  """
  @spec nodes(binary) :: list
  def nodes(service) when is_binary(service) do
    Agent.get(@name, fn(%{services: services}) ->
      case Map.fetch(services, service) do
        :error ->
          []
        {:ok, nodes} ->
          Set.to_list(nodes)
      end
    end)
  end

  @doc """
  List all services and the nodes which provide them.
  """
  @spec services :: Set.t
  def services do
    Agent.get(@name, fn(%{services: services}) -> services end)
  end
end

defmodule Discovery.Directory do
  @moduledoc """
  A registered process that contains the state of known nodes and the services
  that they provide.
  """

  use GenServer
  @name Discovery.Directory

  def start_link do
    Agent.start_link(fn -> %{nodes: %{}, services: %{}} end, name: @name)
  end

  @doc """
  Add a node and the service it provides to the directory.
  """
  @spec add(atom, binary) :: :ok
  def add(node, service) when is_atom(node) and is_binary(service) do
    Agent.update(@name, fn(%{nodes: nodes, services: services} = state) ->
      case Dict.fetch(services, service) do
        :error ->
          new_services = Dict.put(services, service, HashSet.new |> Set.put(node))
        {:ok, nodes} ->
          new_services = Dict.put(services, service, Set.put(nodes, node))
      end

      case Dict.fetch(nodes, node) do
        :error ->
          new_nodes = Dict.put(nodes, node, HashSet.new |> Set.put(service))
        {:ok, node_services} ->
          new_nodes = Dict.put(nodes, node, Set.put(node_services, service))
      end

      %{state | nodes: new_nodes, services: new_services}
    end)
  end

  @doc """
  Clear all registered nodes from the directory.
  """
  @spec clear :: :ok
  def clear do
    Agent.update(@name, fn(_) ->
      %{nodes: %{}, services: %{}}
    end)
  end

  @doc """
  Drop a node from the directory.
  """
  @spec drop(atom) :: :ok
  def drop(node) when is_atom(node) do
    Agent.update(@name, fn(%{nodes: nodes, services: services} = state) ->
      case Dict.pop(nodes, node) do
        {nil, new_nodes} ->
          new_nodes    = new_nodes
          new_services = services
        {_, new_nodes} ->
          new_nodes    = new_nodes
          new_services = Enum.reduce(services, %{}, fn({key, value}, acc) ->
            new_set = Set.delete(value, node)
            case Enum.empty?(new_set) do
              true ->
                acc
              false ->
                Map.put(acc, key, new_set)
            end
          end)
      end

      %{state | nodes: new_nodes, services: new_services}
    end)
  end

  @doc """
  Checks if node exists within the Directory.
  """
  @spec has_node?(atom) :: boolean
  def has_node?(node) when is_atom(node) do
    Agent.get(@name, fn(%{nodes: nodes}) ->
      Map.has_key?(nodes, node)
    end)
  end

  @doc """
  List all nodes and the services they provide.
  """
  @spec nodes :: Set.t
  def nodes do
    Agent.get(@name, fn(%{nodes: nodes}) -> nodes end)
  end

  @doc """
  List all nodes which provide the given service.
  """
  @spec nodes(binary) :: Set.t
  def nodes(service) when is_binary(service) do
    Agent.get(@name, fn(%{services: services}) ->
      case Map.fetch(services, service) do
        :error ->
          HashSet.new
        {:ok, nodes} ->
          nodes
      end
    end)
  end

  @doc """
  List all services and the nodes which provide them.
  """
  @spec services :: Set.t
  def services do
    Agent.get(@name, fn(%{services: services}) -> services end)
  end
end


Remove support for the deprecated :log option

defmodule Redix.StartOptions do
  @moduledoc false

  @default_sentinel_options [
    timeout: 500,
    socket_opts: [],
    ssl: false,
    role: :primary
  ]

  @default_options [
    socket_opts: [],
    ssl: false,
    sync_connect: false,
    backoff_initial: 500,
    backoff_max: 30000,
    exit_on_disconnection: false,
    timeout: 5000
  ]

  @allowed_options [
                     :host,
                     :port,
                     :database,
                     :password,
                     :name,
                     :sentinel,
                     :hibernate_after,
                     :debug,
                     :spawn_opt
                   ] ++
                     Keyword.keys(@default_options)

  def sanitize(options) when is_list(options) do
    @default_options
    |> Keyword.merge(options)
    |> assert_only_known_options()
    |> maybe_sanitize_sentinel_opts()
    |> maybe_sanitize_host_and_port()
  end

  defp assert_only_known_options(options) do
    Enum.each(options, fn {key, _value} ->
      unless key in @allowed_options do
        raise ArgumentError, "unknown option: #{inspect(key)}"
      end
    end)

    options
  end

  defp maybe_sanitize_sentinel_opts(options) do
    case Keyword.fetch(options, :sentinel) do
      {:ok, sentinel_opts} ->
        Keyword.put(options, :sentinel, sanitize_sentinel_opts(options, sentinel_opts))

      :error ->
        options
    end
  end

  defp sanitize_sentinel_opts(opts, sentinel_opts) do
    sentinel_opts =
      case Keyword.fetch(sentinel_opts, :sentinels) do
        {:ok, sentinels} when is_list(sentinels) and sentinels != [] ->
          sentinels = Enum.map(sentinels, &normalize_sentinel_address(&1, sentinel_opts))
          Keyword.replace!(sentinel_opts, :sentinels, sentinels)

        {:ok, sentinels} ->
          raise ArgumentError,
                "the :sentinels option inside :sentinel must be a non-empty list, got: " <>
                  inspect(sentinels)

        :error ->
          raise ArgumentError, "the :sentinels option is required inside :sentinel"
      end

    unless Keyword.has_key?(sentinel_opts, :group) do
      raise ArgumentError, "the :group option is required inside :sentinel"
    end

    if Keyword.has_key?(opts, :host) or Keyword.has_key?(opts, :port) do
      raise ArgumentError, ":host or :port can't be passed as option if :sentinel is used"
    end

    Keyword.merge(@default_sentinel_options, sentinel_opts)
  end

  defp normalize_sentinel_address(sentinel_uri, sentinel_opts) when is_binary(sentinel_uri) do
    sentinel_uri |> Redix.URI.opts_from_uri() |> normalize_sentinel_address(sentinel_opts)
  end

  defp normalize_sentinel_address(opts, sentinel_opts) when is_list(opts) do
    opts =
      if opts[:host] do
        Keyword.update!(opts, :host, &to_charlist/1)
      else
        raise ArgumentError, "a host should be specified for each sentinel"
      end

    unless opts[:port] do
      raise ArgumentError, "a port should be specified for each sentinel"
    end

    Keyword.merge(Keyword.take(sentinel_opts, [:password]), opts)
  end

  defp normalize_sentinel_address(other, _sentinel_opts) do
    raise ArgumentError,
          "sentinel address should be specified as a URI or a keyword list, got: " <>
            inspect(other)
  end

  defp maybe_sanitize_host_and_port(options) do
    if Keyword.has_key?(options, :sentinel) do
      options
    else
      {host, port} =
        case {Keyword.get(options, :host, "localhost"), Keyword.fetch(options, :port)} do
          {{:local, _unix_socket_path}, {:ok, port}} when port != 0 ->
            raise ArgumentError,
                  "when using Unix domain sockets, the port must be 0, got: #{inspect(port)}"

          {{:local, _unix_socket_path} = host, :error} ->
            {host, 0}

          {_host, {:ok, port}} when not is_integer(port) ->
            raise ArgumentError,
                  "expected an integer as the value of the :port option, got: #{inspect(port)}"

          {host, {:ok, port}} when is_binary(host) ->
            {String.to_charlist(host), port}

          {host, :error} when is_binary(host) ->
            {String.to_charlist(host), 6379}
        end

      Keyword.merge(options, host: host, port: port)
    end
  end
end

defmodule Redix.StartOptions do
  @moduledoc false

  @default_sentinel_options [
    timeout: 500,
    socket_opts: [],
    ssl: false,
    role: :primary
  ]

  @default_options [
    socket_opts: [],
    ssl: false,
    sync_connect: false,
    backoff_initial: 500,
    backoff_max: 30000,
    exit_on_disconnection: false,
    timeout: 5000
  ]

  @allowed_options [
                     :host,
                     :port,
                     :database,
                     :password,
                     :name,
                     :sentinel,
                     :hibernate_after,
                     :debug,
                     :spawn_opt
                   ] ++
                     Keyword.keys(@default_options)

  def sanitize(options) when is_list(options) do
    # TODO: remove support for :log in Redix v0.11+.
    options =
      if Keyword.has_key?(options, :log) do
        IO.warn(
          "The :log option has been deprecated in favour of using :telemetry. " <>
            "See https://hexdocs.pm/redix/telemetry.html for more information."
        )

        Keyword.delete(options, :log)
      else
        options
      end

    @default_options
    |> Keyword.merge(options)
    |> assert_only_known_options()
    |> maybe_sanitize_sentinel_opts()
    |> maybe_sanitize_host_and_port()
  end

  defp assert_only_known_options(options) do
    Enum.each(options, fn {key, _value} ->
      unless key in @allowed_options do
        raise ArgumentError, "unknown option: #{inspect(key)}"
      end
    end)

    options
  end

  defp maybe_sanitize_sentinel_opts(options) do
    case Keyword.fetch(options, :sentinel) do
      {:ok, sentinel_opts} ->
        Keyword.put(options, :sentinel, sanitize_sentinel_opts(options, sentinel_opts))

      :error ->
        options
    end
  end

  defp sanitize_sentinel_opts(opts, sentinel_opts) do
    sentinel_opts =
      case Keyword.fetch(sentinel_opts, :sentinels) do
        {:ok, sentinels} when is_list(sentinels) and sentinels != [] ->
          sentinels = Enum.map(sentinels, &normalize_sentinel_address(&1, sentinel_opts))
          Keyword.replace!(sentinel_opts, :sentinels, sentinels)

        {:ok, sentinels} ->
          raise ArgumentError,
                "the :sentinels option inside :sentinel must be a non-empty list, got: " <>
                  inspect(sentinels)

        :error ->
          raise ArgumentError, "the :sentinels option is required inside :sentinel"
      end

    unless Keyword.has_key?(sentinel_opts, :group) do
      raise ArgumentError, "the :group option is required inside :sentinel"
    end

    if Keyword.has_key?(opts, :host) or Keyword.has_key?(opts, :port) do
      raise ArgumentError, ":host or :port can't be passed as option if :sentinel is used"
    end

    Keyword.merge(@default_sentinel_options, sentinel_opts)
  end

  defp normalize_sentinel_address(sentinel_uri, sentinel_opts) when is_binary(sentinel_uri) do
    sentinel_uri |> Redix.URI.opts_from_uri() |> normalize_sentinel_address(sentinel_opts)
  end

  defp normalize_sentinel_address(opts, sentinel_opts) when is_list(opts) do
    opts =
      if opts[:host] do
        Keyword.update!(opts, :host, &to_charlist/1)
      else
        raise ArgumentError, "a host should be specified for each sentinel"
      end

    unless opts[:port] do
      raise ArgumentError, "a port should be specified for each sentinel"
    end

    Keyword.merge(Keyword.take(sentinel_opts, [:password]), opts)
  end

  defp normalize_sentinel_address(other, _sentinel_opts) do
    raise ArgumentError,
          "sentinel address should be specified as a URI or a keyword list, got: " <>
            inspect(other)
  end

  defp maybe_sanitize_host_and_port(options) do
    if Keyword.has_key?(options, :sentinel) do
      options
    else
      {host, port} =
        case {Keyword.get(options, :host, "localhost"), Keyword.fetch(options, :port)} do
          {{:local, _unix_socket_path}, {:ok, port}} when port != 0 ->
            raise ArgumentError,
                  "when using Unix domain sockets, the port must be 0, got: #{inspect(port)}"

          {{:local, _unix_socket_path} = host, :error} ->
            {host, 0}

          {_host, {:ok, port}} when not is_integer(port) ->
            raise ArgumentError,
                  "expected an integer as the value of the :port option, got: #{inspect(port)}"

          {host, {:ok, port}} when is_binary(host) ->
            {String.to_charlist(host), port}

          {host, :error} when is_binary(host) ->
            {String.to_charlist(host), 6379}
        end

      Keyword.merge(options, host: host, port: port)
    end
  end
end