Fix test for traits and tratis_any

Fix test for traits and tratis_any

require Logger

defmodule Serverboards.Plugin.Registry do
  use GenServer

  def start_link(options \\ []) do
    {:ok, pid} = GenServer.start_link(__MODULE__, [], [name: Serverboards.Plugin.Registry] ++ options)

    {:ok, pid}
  end

  def plugin_paths() do
    paths = case Serverboards.Config.get(:plugins, :path, nil) do
      nil ->
        (
          Application.fetch_env!(:serverboards, :plugin_paths)
          ++
          [Path.join(Serverboards.Config.serverboards_path, "plugins")]
        )
      paths ->
        String.split(paths,";")
          |> Enum.map(&String.trim/1)
          |> Enum.filter(&(&1 != ""))
    end

    paths
      |> Enum.filter(&File.dir?/1)
  end

  @doc ~S"""
  Load all plugin description from manifests.
  """
  def load_plugins() do
    paths = plugin_paths()
    plugins = Enum.flat_map paths, fn path ->
      path = Path.expand path
      Logger.debug("Loading plugins from #{inspect path}")
      case Serverboards.Plugin.Parser.read_dir(path) do
        {:ok, plugins} -> plugins
        {:error, err} ->
          Logger.error("Error loading plugin directory #{path}: #{inspect err}")
          []
      end
    end
    plugins
  end

  @doc ~S"""
  Reload all plugin data. Normally called from the inotify watcher
  """
  def reload_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:reload})
  end

  def active_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:get_active})
  end

  def all_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:get_all})
  end

  @doc ~S"""
  Gets component acording to some query from the general plugin registry.

  Can get by id or trait.

  Trait can be :none for component without traits

  ## Example

    iex> [auth] = filter_component id: "fake"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component trait: "auth"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component traits_any: ["anything", "auth"]
    iex> [] = filter_component traits: ["anything", "auth"]
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component trait: "auth", id: "fake"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> filter_component trait: "XXX"
    []

  """
  def filter_component(q) do
    import Enum
    alias Serverboards.Plugin.Component

    components = active_plugins() |>
      flat_map(fn p -> # maps plugins to list of components that fit, or []
        p.components
          |> filter(fn c ->
            #Logger.debug("Check component #{inspect q }: #{inspect c}")

            all? (for {k,v} <- q do
              case k do
                :id ->
                  cid = Map.get(c, :id)
                  (cid == v) or (v == ("#{p.id}/#{cid}"))
                :trait ->
                  member? Map.get(c, :traits), v
                :traits -> # any of the traits fit
                  if v == :none do
                    Map.get(c, :traits) == []
                  else
                    all? v, &(&1 in Map.get(c, :traits))
                  end
                :traits_any -> # any of the traits fit
                  any? v, &(&1 in Map.get(c, :traits))
                :type ->
                  Map.get(c, :type) == v
              end
            end)
          end)
          |> map(fn c ->
            %Component{ c | plugin: p.id, id: "#{p.id}/#{c.id}" }
          end)
      end)
    #Logger.debug("Filter #{inspect q }: #{inspect components}")
    components
  end

  @doc ~S"""
  Looks for a specific component or plugin.

  ## Example

  Find it.

    iex> c = find("serverboards.test.auth/fake")
    iex> c.id
    "serverboards.test.auth/fake"
    iex> c.type
    "cmd"
    iex> c.plugin != nil
    true

  Dont find it. (using global registry)

    iex> find("serverboards.test.auth/authx")
    nil
    iex> find("serverboards.test.authx/fake")
    nil

  Look for plugin

    iex> plugin = find("serverboards.test.auth")
    iex> (Enum.count plugin.components) > 0
    true

  """
  def find(nil), do: nil
  def find(id) do
    alias Serverboards.Plugin

    # on the form .*/.*
    case Regex.run(~r"^([^/]+)/([^/]+)$", id ) do
      nil ->
        case Regex.run(~r"^[^/]+$", id ) do
          nil ->
            nil
          [plugin_id] ->
            Enum.find(active_plugins(), &(&1.id == plugin_id) )
        end
      [_, plugin_id, component_id] ->
        with %Plugin{} = plugin <- Enum.find(active_plugins(), &(&1.id == plugin_id) ),
             %Plugin.Component{} = component <- Enum.find(plugin.components, &(&1.id == component_id) )
        do
           %Plugin.Component{ component | plugin: plugin, id: "#{plugin.id}/#{component.id}" }
        else
          _ -> nil
        end
    end
  end

  def get_plugin_status( id, context ) do
    broken = Map.get(context.broken, id, false)
    active = (
      String.starts_with?(id, "serverboards.core") ||
      Map.get(context.active, id, true)
      )

    status = cond do
      broken ->
        ["disabled", "broken"]
      active ->
        ["active"]
      true ->
        ["disabled"]
    end

    status
  end

  def list() do
    Enum.reduce(all_plugins(), %{}, fn plugin, acc ->
      components = Enum.reduce(plugin.components, %{}, fn component, acc ->
        Map.put(acc, component.id, %{
          id: component.id,
          name: component.name,
          type: component.type,
          traits: component.traits,
          description: component.description
          })
      end)
      Map.put(acc, plugin.id, %{
        version: plugin.version,
        name: plugin.name,
        id: plugin.id,
        author: plugin.author,
        description: plugin.description,
        url: plugin.url,
        status: plugin.status,
        components: components,
        extra: plugin.extra
        })
    end)
  end

  ## server impl, just stores state
  def init([]) do
    pid = self()
    MOM.Channel.subscribe(:settings, fn
      %MOM.Message{ payload: %{ type: :update, section: section } = payload} ->
        if section == "plugins" do
          Logger.debug("Reloading plugins, active plugins settings has changed")
          GenServer.cast(pid, {:reload, %{ plugins: payload.data }})
        end
        if section == "broken_plugins" do
          Logger.debug("Reloading plugins, broken plugins settings has changed")
          GenServer.cast(pid, {:reload, %{ broken_plugins: payload.data }})
        end
      _ -> :ignore
    end)

    GenServer.cast(self(), {:reload})
    {:ok, %{ active: [], all: []}}
  end

  def decorate_plugin(p, context) do
    %{
      p |
      status: get_plugin_status(p.id, context)
    }
  end

  def handle_call({:reload}, _from, status) do
    {:noreply, status} = handle_cast({:reload}, status)
    {:reply, :ok, status}
  end
  def handle_call({:get_all}, _from, state) do
    {:reply, state.all, state}
  end
  def handle_call({:get_active}, _from, state) do
    {:reply, state.active, state}
  end

  def handle_cast({:reload}, status), do: handle_cast({:reload, %{}}, status)
  def handle_cast({:reload, context}, _status) do
    Logger.debug("Reloading plugin lists.")
    active = case Map.get(context, :plugins) do
      nil -> Serverboards.Config.get_map(:plugins)
      value -> value
    end
    broken = case Map.get(context, :broken_plugins) do
      nil -> Serverboards.Config.get_map(:broken_plugins)
      value -> value
    end

    context = %{
      active: active,
      broken: broken,
    }

    all_plugins = load_plugins()
      |> Enum.map(&decorate_plugin(&1, context))
    active = Enum.filter(all_plugins, &(&1.status))

    st = for i <- all_plugins do
      {i.id, i.status}
    end

    Serverboards.Event.emit("plugins.reloaded", nil, ["plugin"])

    {:noreply, %{
      all: all_plugins,
      active: active
      }}
  end
end

require Logger

defmodule Serverboards.Plugin.Registry do
  use GenServer

  def start_link(options \\ []) do
    {:ok, pid} = GenServer.start_link(__MODULE__, [], [name: Serverboards.Plugin.Registry] ++ options)

    {:ok, pid}
  end

  def plugin_paths() do
    paths = case Serverboards.Config.get(:plugins, :path, nil) do
      nil ->
        (
          Application.fetch_env!(:serverboards, :plugin_paths)
          ++
          [Path.join(Serverboards.Config.serverboards_path, "plugins")]
        )
      paths ->
        String.split(paths,";")
          |> Enum.map(&String.trim/1)
          |> Enum.filter(&(&1 != ""))
    end

    paths
      |> Enum.filter(&File.dir?/1)
  end

  @doc ~S"""
  Load all plugin description from manifests.
  """
  def load_plugins() do
    paths = plugin_paths()
    plugins = Enum.flat_map paths, fn path ->
      path = Path.expand path
      Logger.debug("Loading plugins from #{inspect path}")
      case Serverboards.Plugin.Parser.read_dir(path) do
        {:ok, plugins} -> plugins
        {:error, err} ->
          Logger.error("Error loading plugin directory #{path}: #{inspect err}")
          []
      end
    end
    plugins
  end

  @doc ~S"""
  Reload all plugin data. Normally called from the inotify watcher
  """
  def reload_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:reload})
  end

  def active_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:get_active})
  end

  def all_plugins do
    GenServer.call(Serverboards.Plugin.Registry, {:get_all})
  end

  @doc ~S"""
  Gets component acording to some query from the general plugin registry.

  Can get by id or trait.

  Trait can be :none for component without traits

  ## Example

    iex> [auth] = filter_component id: "fake"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component trait: "auth"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component traits: ["anything", "auth"]
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> [auth] = filter_component trait: "auth", id: "fake"
    iex> auth.id
    "serverboards.test.auth/fake"
    iex> filter_component trait: "XXX"
    []

  """
  def filter_component(q) do
    import Enum
    alias Serverboards.Plugin.Component

    components = active_plugins() |>
      flat_map(fn p -> # maps plugins to list of components that fit, or []
        p.components
          |> filter(fn c ->
            #Logger.debug("Check component #{inspect q }: #{inspect c}")

            all? (for {k,v} <- q do
              case k do
                :id ->
                  cid = Map.get(c, :id)
                  (cid == v) or (v == ("#{p.id}/#{cid}"))
                :trait ->
                  member? Map.get(c, :traits), v
                :traits -> # any of the traits fit
                  if v == :none do
                    Map.get(c, :traits) == []
                  else
                    all? v, &(&1 in Map.get(c, :traits))
                  end
                :traits_any -> # any of the traits fit
                  any? v, &(&1 in Map.get(c, :traits))
                :type ->
                  Map.get(c, :type) == v
              end
            end)
          end)
          |> map(fn c ->
            %Component{ c | plugin: p.id, id: "#{p.id}/#{c.id}" }
          end)
      end)
    #Logger.debug("Filter #{inspect q }: #{inspect components}")
    components
  end

  @doc ~S"""
  Looks for a specific component or plugin.

  ## Example

  Find it.

    iex> c = find("serverboards.test.auth/fake")
    iex> c.id
    "serverboards.test.auth/fake"
    iex> c.type
    "cmd"
    iex> c.plugin != nil
    true

  Dont find it. (using global registry)

    iex> find("serverboards.test.auth/authx")
    nil
    iex> find("serverboards.test.authx/fake")
    nil

  Look for plugin

    iex> plugin = find("serverboards.test.auth")
    iex> (Enum.count plugin.components) > 0
    true

  """
  def find(nil), do: nil
  def find(id) do
    alias Serverboards.Plugin

    # on the form .*/.*
    case Regex.run(~r"^([^/]+)/([^/]+)$", id ) do
      nil ->
        case Regex.run(~r"^[^/]+$", id ) do
          nil ->
            nil
          [plugin_id] ->
            Enum.find(active_plugins(), &(&1.id == plugin_id) )
        end
      [_, plugin_id, component_id] ->
        with %Plugin{} = plugin <- Enum.find(active_plugins(), &(&1.id == plugin_id) ),
             %Plugin.Component{} = component <- Enum.find(plugin.components, &(&1.id == component_id) )
        do
           %Plugin.Component{ component | plugin: plugin, id: "#{plugin.id}/#{component.id}" }
        else
          _ -> nil
        end
    end
  end

  def get_plugin_status( id, context ) do
    broken = Map.get(context.broken, id, false)
    active = (
      String.starts_with?(id, "serverboards.core") ||
      Map.get(context.active, id, true)
      )

    status = cond do
      broken ->
        ["disabled", "broken"]
      active ->
        ["active"]
      true ->
        ["disabled"]
    end

    status
  end

  def list() do
    Enum.reduce(all_plugins(), %{}, fn plugin, acc ->
      components = Enum.reduce(plugin.components, %{}, fn component, acc ->
        Map.put(acc, component.id, %{
          id: component.id,
          name: component.name,
          type: component.type,
          traits: component.traits,
          description: component.description
          })
      end)
      Map.put(acc, plugin.id, %{
        version: plugin.version,
        name: plugin.name,
        id: plugin.id,
        author: plugin.author,
        description: plugin.description,
        url: plugin.url,
        status: plugin.status,
        components: components,
        extra: plugin.extra
        })
    end)
  end

  ## server impl, just stores state
  def init([]) do
    pid = self()
    MOM.Channel.subscribe(:settings, fn
      %MOM.Message{ payload: %{ type: :update, section: section } = payload} ->
        if section == "plugins" do
          Logger.debug("Reloading plugins, active plugins settings has changed")
          GenServer.cast(pid, {:reload, %{ plugins: payload.data }})
        end
        if section == "broken_plugins" do
          Logger.debug("Reloading plugins, broken plugins settings has changed")
          GenServer.cast(pid, {:reload, %{ broken_plugins: payload.data }})
        end
      _ -> :ignore
    end)

    GenServer.cast(self(), {:reload})
    {:ok, %{ active: [], all: []}}
  end

  def decorate_plugin(p, context) do
    %{
      p |
      status: get_plugin_status(p.id, context)
    }
  end

  def handle_call({:reload}, _from, status) do
    {:noreply, status} = handle_cast({:reload}, status)
    {:reply, :ok, status}
  end
  def handle_call({:get_all}, _from, state) do
    {:reply, state.all, state}
  end
  def handle_call({:get_active}, _from, state) do
    {:reply, state.active, state}
  end

  def handle_cast({:reload}, status), do: handle_cast({:reload, %{}}, status)
  def handle_cast({:reload, context}, _status) do
    Logger.debug("Reloading plugin lists.")
    active = case Map.get(context, :plugins) do
      nil -> Serverboards.Config.get_map(:plugins)
      value -> value
    end
    broken = case Map.get(context, :broken_plugins) do
      nil -> Serverboards.Config.get_map(:broken_plugins)
      value -> value
    end

    context = %{
      active: active,
      broken: broken,
    }

    all_plugins = load_plugins()
      |> Enum.map(&decorate_plugin(&1, context))
    active = Enum.filter(all_plugins, &(&1.status))

    st = for i <- all_plugins do
      {i.id, i.status}
    end

    Serverboards.Event.emit("plugins.reloaded", nil, ["plugin"])

    {:noreply, %{
      all: all_plugins,
      active: active
      }}
  end
end


Update post index to grab categories appropriately

defmodule Ello.Search.Post.Index do
  alias Ello.Search.Client
  alias Ello.Core.{Repo}

  def create do
    Client.create_index(index_name(), settings())
    Client.put_mapping(index_name(), post_doc_type(), post_mapping())
    Client.put_mapping(index_name(), author_doc_type(), author_mapping())
  end

  def add(post, overrides \\ %{}) do
    post_data   = assemble_post_data(post, overrides)
    author_data = assemble_author_data(post.author, overrides)

    Client.index_document(index_name(), author_doc_type(), post.author.id, author_data)
    Client.index_document(index_name(), post_doc_type(), post.id, post_data, %{parent: post.author.id})
    Client.refresh_index(index_name())
  end

  defp assemble_post_data(post, overrides) do
    post = Repo.preload(post, :categories)
    %{
      id:                post.id,
      created_at:        post.created_at,
      updated_at:        post.updated_at,
      token:             post.token,
      author_id:         post.author_id,
      text_content:      text_content(post),
      hashtags:          hashtags(post),
      mentions:          post.mentioned_usernames,
      detected_language: detected_language(post),
      is_adult_content:  post.is_adult_content,
      has_nudity:        post.has_nudity,
      is_disabled:       post.is_disabled,
      is_hidden:         false,
      is_repost:         !!post.reposted_source,
      is_comment:        !!post.parent_post,
      comment_count:     0,
      repost_count:      0,
      love_count:        0,
      view_count:        0,
      alt_text:          "",
      is_saleable:       post.is_saleable,
      category_ids:      Enum.map(post.categories, &(&1.id)),
      has_images:        has_images(post),
    } |> Map.merge(overrides[:post] || %{})
  end

  defp assemble_author_data(author, overrides) do
    author = Repo.preload(author, :categories)
    %{
      id:                 author.id,
      created_at:         author.created_at,
      updated_at:         author.updated_at,
      name:               author.name,
      username:           author.username,
      short_bio:          author.short_bio,
      links:              author.links,
      locked_out:         !!author.locked_at,
      post_count:         0,
      comment_count:      0,
      follower_count:     0,
      is_spammer:         false,
      is_system_user:     author.is_system_user,
      is_featured_user:   Enum.any?(author.categories),
      has_avatar:         !!author.avatar,
      is_public:          author.is_public,
      category_ids:       Enum.map(author.categories, &(&1.id)),
      category_names:     Enum.map(author.categories, &(&1.name)),
      is_hireable:        author.settings.is_hireable,
      is_collaborateable: author.settings.is_collaborateable,
      location:           author.location,
      coordinates:        coordinates(author)
    } |> Map.merge(overrides[:author] || %{})
  end

  def delete, do: Client.delete_index(index_name())

  def index_name,      do: Application.get_env(:ello_search, :posts_active_index_name)
  def author_doc_type, do: "author"
  def post_doc_type,   do: "post"
  def doc_types,       do: [post_doc_type(), author_doc_type()]
  def settings do
    %{
      settings: %{
        index: %{
          number_of_shards: Application.get_env(:ello_search, :es_default_shards),
          number_of_replicas: Application.get_env(:ello_search, :es_default_replicas)
        },
        analysis: %{
          analyzer: %{
            default_text_analyzer: %{
              type: "custom",
              tokenizer: "standard",
              filter: ["lowercase"]
            }
          }
        }
      }
    }
  end

  def author_mapping do
    %{
      properties: %{
        id:                 %{type: "integer"},
        created_at:         %{type: "date"},
        updated_at:         %{type: "date"},
        name:               %{type: "text"},
        username:           %{type: "text"},
        short_bio:          %{type: "text"},
        links:              %{type: "text"},
        locked_out:         %{type: "boolean"},
        post_count:         %{type: "integer"},
        comment_count:      %{type: "integer"},
        follower_count:     %{type: "integer"},
        is_spammer:         %{type: "boolean"},
        is_system_user:     %{type: "boolean"},
        is_featured_user:   %{type: "boolean"},
        has_avatar:         %{type: "boolean"},
        is_public:          %{type: "boolean"},
        category_ids:       %{type: "text"},
        category_names:     %{type: "text"},
        is_hireable:        %{type: "boolean"},
        is_collaborateable: %{type: "boolean"},
        location:           %{type: "text"},
        coordinates:        %{type: "geo_point"}
      }
    }
  end

  def post_mapping do
    %{
      "_parent" => %{type: author_doc_type()},
      properties: %{
        id:                %{type: "integer"},
        created_at:        %{type: "date"},
        updated_at:        %{type: "date"},
        token:             %{type: "text"},
        author_id:         %{type: "integer"},
        text_content: %{
          type:     "text",
          fields:   %{raw: %{type: "text", analyzer: "default_text_analyzer"}},
          analyzer: "english",
        },
        hashtags:          %{type: "text"},
        mentions:          %{type: "text"},
        detected_language: %{type: "text", index: false},
        is_adult_content:  %{type: "boolean"},
        has_nudity:        %{type: "boolean"},
        is_disabled:       %{type: "boolean"},
        is_hidden:         %{type: "boolean"},
        is_repost:         %{type: "boolean"},
        is_comment:        %{type: "boolean"},
        comment_count:     %{type: "integer"},
        repost_count:      %{type: "integer"},
        love_count:        %{type: "integer"},
        view_count:        %{type: "integer"},
        alt_text:          %{type: "text", analyzer: "english"},
        is_saleable:       %{type: "boolean"},
        has_images:        %{type: "boolean"},
      }
    }
  end

  defp text_content(%{reposted_source: reposted_source} = post) when reposted_source != nil do
    text_content(post, text_data(post.reposted_source.body))
  end
  defp text_content(post, repost_text_data \\ []) do
    repost_text_data ++ text_data(post.body)
    |> Enum.map_join(" ", &(String.trim(&1["data"])))
    |> HtmlSanitizeEx.strip_tags
    |> String.trim
  end

  defp text_data(body) when is_list(body), do: Enum.filter(body, &(&1["kind"] == "text"))

  defp hashtags(post) do
    case Regex.run(~r/\B(#\w+)/, text_content(post, [])) do
      nil     -> []
      results -> results
                 |> Enum.map(&(String.downcase(&1)))
                 |> Enum.uniq
    end
  end

  defp coordinates(%{location_lat: nil, location_long: nil}), do: nil
  defp coordinates(%{location_lat: lat, location_long: long}), do: %{lat: lat, lon: long}

  defp detected_language(_post), do: "en"

  defp has_images(%{body: body, reposted_source: %{body: reposted_body}}) do
    Enum.any?(body ++ reposted_body, &(&1["kind" == "image"]))
  end
  defp has_images(%{body: body}) do
    Enum.any?(body, &(&1["kind"] == "image"))
  end
end

defmodule Ello.Search.Post.Index do
  alias Ello.Search.Client
  alias Ello.Core.{Discovery, Repo}

  def create do
    Client.create_index(index_name(), settings())
    Client.put_mapping(index_name(), post_doc_type(), post_mapping())
    Client.put_mapping(index_name(), author_doc_type(), author_mapping())
  end

  def add(post, overrides \\ %{}) do
    post_data   = assemble_post_data(post, overrides)
    author_data = assemble_author_data(post.author, overrides)

    Client.index_document(index_name(), author_doc_type(), post.author.id, author_data)
    Client.index_document(index_name(), post_doc_type(), post.id, post_data, %{parent: post.author.id})
    Client.refresh_index(index_name())
  end

  defp assemble_post_data(post, overrides) do
    post = Repo.preload(post, :categories)
    %{
      id:                post.id,
      created_at:        post.created_at,
      updated_at:        post.updated_at,
      token:             post.token,
      author_id:         post.author_id,
      text_content:      text_content(post),
      hashtags:          hashtags(post),
      mentions:          post.mentioned_usernames,
      detected_language: detected_language(post),
      is_adult_content:  post.is_adult_content,
      has_nudity:        post.has_nudity,
      is_disabled:       post.is_disabled,
      is_hidden:         false,
      is_repost:         !!post.reposted_source,
      is_comment:        !!post.parent_post,
      comment_count:     0,
      repost_count:      0,
      love_count:        0,
      view_count:        0,
      alt_text:          "",
      is_saleable:       post.is_saleable,
      category_ids:      Enum.map(post.categories, &(&1.id)),
      has_images:        has_images(post),
    } |> Map.merge(overrides[:post] || %{})
  end

  defp assemble_author_data(author, overrides) do
    %{
      id:                 author.id,
      created_at:         author.created_at,
      updated_at:         author.updated_at,
      name:               author.name,
      username:           author.username,
      short_bio:          author.short_bio,
      links:              author.links,
      locked_out:         !!author.locked_at,
      post_count:         0,
      comment_count:      0,
      follower_count:     0,
      is_spammer:         false,
      is_system_user:     author.is_system_user,
      is_featured_user:   Enum.any?(author.category_ids),
      has_avatar:         !!author.avatar,
      is_public:          author.is_public,
      category_ids:       author.category_ids,
      category_names:     category_names(author.category_ids),
      is_hireable:        author.settings.is_hireable,
      is_collaborateable: author.settings.is_collaborateable,
      location:           author.location,
      coordinates:        coordinates(author)
    } |> Map.merge(overrides[:author] || %{})
  end

  def delete, do: Client.delete_index(index_name())

  def index_name,      do: Application.get_env(:ello_search, :posts_active_index_name)
  def author_doc_type, do: "author"
  def post_doc_type,   do: "post"
  def doc_types,       do: [post_doc_type(), author_doc_type()]
  def settings do
    %{
      settings: %{
        index: %{
          number_of_shards: Application.get_env(:ello_search, :es_default_shards),
          number_of_replicas: Application.get_env(:ello_search, :es_default_replicas)
        },
        analysis: %{
          analyzer: %{
            default_text_analyzer: %{
              type: "custom",
              tokenizer: "standard",
              filter: ["lowercase"]
            }
          }
        }
      }
    }
  end

  def author_mapping do
    %{
      properties: %{
        id:                 %{type: "integer"},
        created_at:         %{type: "date"},
        updated_at:         %{type: "date"},
        name:               %{type: "text"},
        username:           %{type: "text"},
        short_bio:          %{type: "text"},
        links:              %{type: "text"},
        locked_out:         %{type: "boolean"},
        post_count:         %{type: "integer"},
        comment_count:      %{type: "integer"},
        follower_count:     %{type: "integer"},
        is_spammer:         %{type: "boolean"},
        is_system_user:     %{type: "boolean"},
        is_featured_user:   %{type: "boolean"},
        has_avatar:         %{type: "boolean"},
        is_public:          %{type: "boolean"},
        category_ids:       %{type: "text"},
        category_names:     %{type: "text"},
        is_hireable:        %{type: "boolean"},
        is_collaborateable: %{type: "boolean"},
        location:           %{type: "text"},
        coordinates:        %{type: "geo_point"}
      }
    }
  end

  def post_mapping do
    %{
      "_parent" => %{type: author_doc_type()},
      properties: %{
        id:                %{type: "integer"},
        created_at:        %{type: "date"},
        updated_at:        %{type: "date"},
        token:             %{type: "text"},
        author_id:         %{type: "integer"},
        text_content: %{
          type:     "text",
          fields:   %{raw: %{type: "text", analyzer: "default_text_analyzer"}},
          analyzer: "english",
        },
        hashtags:          %{type: "text"},
        mentions:          %{type: "text"},
        detected_language: %{type: "text", index: false},
        is_adult_content:  %{type: "boolean"},
        has_nudity:        %{type: "boolean"},
        is_disabled:       %{type: "boolean"},
        is_hidden:         %{type: "boolean"},
        is_repost:         %{type: "boolean"},
        is_comment:        %{type: "boolean"},
        comment_count:     %{type: "integer"},
        repost_count:      %{type: "integer"},
        love_count:        %{type: "integer"},
        view_count:        %{type: "integer"},
        alt_text:          %{type: "text", analyzer: "english"},
        is_saleable:       %{type: "boolean"},
        has_images:        %{type: "boolean"},
      }
    }
  end

  defp category_names(category_ids) when length(category_ids) == 0, do: []
  defp category_names(category_ids) do
    %{ids: category_ids, images: false}
    |> Discovery.categories
    |> Enum.map(&(&1.name))
  end

  defp text_content(%{reposted_source: reposted_source} = post) when reposted_source != nil do
    text_content(post, text_data(post.reposted_source.body))
  end
  defp text_content(post, repost_text_data \\ []) do
    repost_text_data ++ text_data(post.body)
    |> Enum.map_join(" ", &(String.trim(&1["data"])))
    |> HtmlSanitizeEx.strip_tags
    |> String.trim
  end

  defp text_data(body) when is_list(body), do: Enum.filter(body, &(&1["kind"] == "text"))

  defp hashtags(post) do
    case Regex.run(~r/\B(#\w+)/, text_content(post, [])) do
      nil     -> []
      results -> results
                 |> Enum.map(&(String.downcase(&1)))
                 |> Enum.uniq
    end
  end

  defp coordinates(%{location_lat: nil, location_long: nil}), do: nil
  defp coordinates(%{location_lat: lat, location_long: long}), do: %{lat: lat, lon: long}

  defp detected_language(_post), do: "en"

  defp has_images(%{body: body, reposted_source: %{body: reposted_body}}) do
    Enum.any?(body ++ reposted_body, &(&1["kind" == "image"]))
  end
  defp has_images(%{body: body}) do
    Enum.any?(body, &(&1["kind"] == "image"))
  end
end


add missing newline to end of file

add missing newline to end of file

defmodule <%= application_module %>.LayoutViewTest do
  use <%= application_module %>.ConnCase, async: true
end


Fixed tests for new issues count

Fixed tests for new issues count

require Logger

defmodule Serverboards.IssuesTest do
  use ExUnit.Case
  @moduletag :capture_log

  alias Serverboards.Issues.Issue
  alias Serverboards.Issues

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Serverboards.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(Serverboards.Repo, {:shared, self()})
    %{}
  end

  test "Create and update basic issue" do
    {:ok, user} = Serverboards.Auth.User.user_info "dmoreno@serverboards.io"
    {:ok, id} = Issue.add %{ title: "Issue test", description: "This is the description of the issue." }, user
    assert id > 0

    {:ok, issue} = Issue.get(id)

    assert issue.id == id
    assert issue.creator.email == "dmoreno@serverboards.io"
    assert Enum.count(issue.events) == 1
    assert Enum.at(issue.events,0).type == "new_issue"
    assert Enum.at(issue.events,0).data == %{ "description" => "This is the description of the issue.", "title" => "Issue test" }

    {:ok, _} = Issue.update id, %{ type: "comment", data: "Good remark.\n\nThanks for the issue report." }, user
    {:ok, issue} = Issue.get(id)
    assert Enum.count(issue.events) == 2
    assert Enum.at(issue.events,1).type == "comment"
    assert Enum.at(issue.events,1).data == "Good remark.\n\nThanks for the issue report."

    issues = Issues.list
    assert Enum.count(issues) > 0
  end

  test "Create issue using RPC" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    assert issue_id > 0
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    Logger.info(inspect issue)
    assert issue["title"] == "From RPC"
    assert issue["events"] != []

    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, %{ type: :comment, data: %{ comment: "A comment.\n\nFull."}}])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    Logger.info(inspect issue)

    assert issue["title"] == "From RPC"
    assert Enum.count(issue["events"]) == 2

    {:ok, issues} = Test.Client.call(client, "issues.list", [])

    Logger.info(inspect issues)

  end

  test "Create issue with alias" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From alias", description: "This is a new issue", aliases: ["test/1111"] })
    {:ok, issue} = Test.Client.call(client, "issues.get", ["test/1111"])

    assert issue["id"] == issue_id

    {:ok, _issue} = Test.Client.call(client, "issues.update", ["test/1111", %{ type: :comment, data: "A comment.\n\nFull."}])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])

    Logger.info(inspect issue)

    assert issue["title"] == "From alias"

    # can be listed by alias
    {:ok, issues} = Test.Client.call(client, "issues.list", %{ alias: "test/1111"})
    Logger.info(inspect issues)

    assert Enum.count(issues) > 0
  end

  test "Status changes" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Closing issue"},
        %{ type: :change_status, data: "closed"}
      ]])

    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])

    Logger.info(inspect issue)

    assert issue["status"] == "closed"
  end

  test "Set labels" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Set labels"},
        %{ type: :set_labels, data: ["one", "two"]}
      ]])

    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    assert Enum.map(issue["labels"], &(&1["name"])) == ["one","two"]

    # Now list, get labels
    {:ok, issues} = Test.Client.call(client, "issues.list", [])
    Logger.info(inspect issues)

    assert Enum.find(issues, &(&1["id"]==issue["id"]))["labels"]==issue["labels"]

    # unset
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Unset labels"},
        %{ type: :unset_labels, data: ["one"]}
      ]])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    assert Enum.map(issue["labels"], &(&1["name"])) == ["two"]
  end

  test "Complex lists" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    Test.Client.call(client, "event.subscribe", ["issue.created", "issue.updated"])
    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "At least one for tests", description: "Test issue", aliases: ["project/TEST"] })

    res = Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", return: "count" })
    Logger.debug(inspect res)

    # assert with placeholders
    {:ok, %{ "count" => 1, "timestamp" => _ }} = res

    {:ok, list} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", project: "TEST" })
    Logger.debug(inspect list)
    issue = List.first(list)

    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2050-01-01", project: "TEST" })
    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", count: 0, project: "TEST" })

    {:ok, _issue_id} = Test.Client.call(client, "issues.create", %{ title: "Second one for since test", description: "Test since issue", aliases: ["project/TEST"] })
    {:ok, [issue2]} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    assert Test.Client.expect(client, method: "issue.created")

    # update issue1 with a comment, should update updated_at.
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Set labels"},
        %{ type: :set_labels, data: ["one"]}
      ]])
    assert Test.Client.expect(client, method: "issue.updated")

    {:ok, since_items} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    Logger.info("Got #{Enum.count(since_items)} // #{inspect since_items, pretty: true} from #{inspect issue["updated_at"], pretty: true}")
    # {:ok, res} = Test.Client.call(client, "issues.list", %{ project: "TEST", project: "TEST" })
    Logger.debug("All #{inspect res} from #{inspect issue["updated_at"]}")
    {:ok, %{ "count" => count }} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], return: "count" })
    Logger.info("Count #{inspect count}")
    assert 1 == count
  end

end

require Logger

defmodule Serverboards.IssuesTest do
  use ExUnit.Case
  @moduletag :capture_log

  alias Serverboards.Issues.Issue
  alias Serverboards.Issues

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(Serverboards.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(Serverboards.Repo, {:shared, self()})
    %{}
  end

  test "Create and update basic issue" do
    {:ok, user} = Serverboards.Auth.User.user_info "dmoreno@serverboards.io"
    {:ok, id} = Issue.add %{ title: "Issue test", description: "This is the description of the issue." }, user
    assert id > 0

    {:ok, issue} = Issue.get(id)

    assert issue.id == id
    assert issue.creator.email == "dmoreno@serverboards.io"
    assert Enum.count(issue.events) == 1
    assert Enum.at(issue.events,0).type == "new_issue"
    assert Enum.at(issue.events,0).data == %{ "description" => "This is the description of the issue.", "title" => "Issue test" }

    {:ok, _} = Issue.update id, %{ type: "comment", data: "Good remark.\n\nThanks for the issue report." }, user
    {:ok, issue} = Issue.get(id)
    assert Enum.count(issue.events) == 2
    assert Enum.at(issue.events,1).type == "comment"
    assert Enum.at(issue.events,1).data == "Good remark.\n\nThanks for the issue report."

    issues = Issues.list
    assert Enum.count(issues) > 0
  end

  test "Create issue using RPC" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    assert issue_id > 0
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    Logger.info(inspect issue)
    assert issue["title"] == "From RPC"
    assert issue["events"] != []

    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, %{ type: :comment, data: %{ comment: "A comment.\n\nFull."}}])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    Logger.info(inspect issue)

    assert issue["title"] == "From RPC"
    assert Enum.count(issue["events"]) == 2

    {:ok, issues} = Test.Client.call(client, "issues.list", [])

    Logger.info(inspect issues)

  end

  test "Create issue with alias" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From alias", description: "This is a new issue", aliases: ["test/1111"] })
    {:ok, issue} = Test.Client.call(client, "issues.get", ["test/1111"])

    assert issue["id"] == issue_id

    {:ok, _issue} = Test.Client.call(client, "issues.update", ["test/1111", %{ type: :comment, data: "A comment.\n\nFull."}])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])

    Logger.info(inspect issue)

    assert issue["title"] == "From alias"

    # can be listed by alias
    {:ok, issues} = Test.Client.call(client, "issues.list", %{ alias: "test/1111"})
    Logger.info(inspect issues)

    assert Enum.count(issues) > 0
  end

  test "Status changes" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Closing issue"},
        %{ type: :change_status, data: "closed"}
      ]])

    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])

    Logger.info(inspect issue)

    assert issue["status"] == "closed"
  end

  test "Set labels" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "From RPC", description: "This is a new issue" })
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Set labels"},
        %{ type: :set_labels, data: ["one", "two"]}
      ]])

    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    assert Enum.map(issue["labels"], &(&1["name"])) == ["one","two"]

    # Now list, get labels
    {:ok, issues} = Test.Client.call(client, "issues.list", [])
    Logger.info(inspect issues)

    assert Enum.find(issues, &(&1["id"]==issue["id"]))["labels"]==issue["labels"]

    # unset
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Unset labels"},
        %{ type: :unset_labels, data: ["one"]}
      ]])
    {:ok, issue} = Test.Client.call(client, "issues.get", [issue_id])
    assert Enum.map(issue["labels"], &(&1["name"])) == ["two"]
  end

  test "Complex lists" do
    {:ok, client} = Test.Client.start_link as: "dmoreno@serverboards.io"

    Test.Client.call(client, "event.subscribe", ["issue.created", "issue.updated"])
    {:ok, issue_id} = Test.Client.call(client, "issues.create", %{ title: "At least one for tests", description: "Test issue", aliases: ["project/TEST"] })

    res = Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", return: "count" })
    Logger.debug(inspect res)
    assert {:ok, 1} == res

    {:ok, list} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", project: "TEST" })
    Logger.debug(inspect list)
    issue = List.first(list)

    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2050-01-01", project: "TEST" })
    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    assert {:ok, []} == Test.Client.call(client, "issues.list", %{ project: "TEST", since: "2017-01-01", count: 0, project: "TEST" })

    {:ok, _issue_id} = Test.Client.call(client, "issues.create", %{ title: "Second one for since test", description: "Test since issue", aliases: ["project/TEST"] })
    {:ok, [issue2]} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    assert Test.Client.expect(client, method: "issue.created")

    # update issue1 with a comment, should update updated_at.
    {:ok, _issue} = Test.Client.call(client, "issues.update", [issue_id, [
        %{ type: :comment, data: "Set labels"},
        %{ type: :set_labels, data: ["one"]}
      ]])
    assert Test.Client.expect(client, method: "issue.updated")

    {:ok, since_items} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], project: "TEST" })
    Logger.info("Got #{Enum.count(since_items)} // #{inspect since_items, pretty: true} from #{inspect issue["updated_at"], pretty: true}")
    # {:ok, res} = Test.Client.call(client, "issues.list", %{ project: "TEST", project: "TEST" })
    Logger.debug("All #{inspect res} from #{inspect issue["updated_at"]}")
    {:ok, count} = Test.Client.call(client, "issues.list", %{ project: "TEST", since: issue["updated_at"], return: "count" })
    Logger.info("Count #{inspect count}")
    assert 1 == count
  end

end


Switch calendar controller tests to use Calendars context

defmodule CalgyApi.CalendarControllerTest do
  use CalgyApi.ConnCase

  alias Plug.Conn
  alias Calgy.Calendars

  setup %{conn: orig_conn} do
    conn = put_req_header(orig_conn, "accept", "application/json")
    {:ok, %{conn: conn}}
  end

  test "POST creates a pending calendar by default", %{conn: conn} do
    conn = post conn, calendar_path(conn, :create), %{}
    body = json_response(conn, 201)

    {:ok, calendar} = Calendars.get_calendar(body["id"])
    assert calendar

    location = Conn.get_resp_header(conn, "location") |> List.first
    assert location == calendar_url(conn, :show, calendar.admin_id) <> ";admin"

    assert body["state"] == "pending"
    assert body["public_url"] == calendar_url(conn, :show, calendar.id)
  end

  test "POST ignores attempts to set the id", %{conn: conn} do
    id = Ecto.UUID.generate
    conn = post conn, calendar_path(conn, :create), %{id: id}
    assert json_response(conn, 201)["id"] != id
  end

  test "POST allows setting :title and :description", %{conn: conn} do
    attrs = %{title: "my title", description: "my description"}
    conn = post conn, calendar_path(conn, :create), attrs
    body = json_response(conn, 201)

    assert body["title"] == "my title"
    assert body["description"] == "my description"
  end

  test "POST response does not include nil values", %{conn: conn} do
    conn = post conn, calendar_path(conn, :create), %{}
    body = json_response(conn, 201)

    refute Map.has_key?(body, "title")
    refute Map.has_key?(body, "description")
  end

  test "POST returns an error if any attributes are invalid", %{conn: conn} do
    attrs = %{title: String.duplicate("x", 101)}
    conn = post conn, calendar_path(conn, :create), attrs
    body = json_response(conn, 422)
    error = List.first(body["errors"])

    assert error["path"] == "#/title"
    assert error["code"] == "too_long"
  end

  test "GET shows information about a calendar", %{conn: conn} do
    {:ok, calendar} = Calendars.create_calendar()
    conn = get conn, calendar_path(conn, :show, calendar)
    body = json_response(conn, 200)

    assert body["id"]
    assert body["state"]
  end

  test "GET returns 404 if uuid is invalid", %{conn: conn} do
    {:ok, _calendar} = Calendars.create_calendar()
    conn = get conn, calendar_path(conn, :show, "teapot")
    body = json_response(conn, 404)
    assert body["error"]
  end

  test "GET retuns 404 if calendar is not found", %{conn: conn} do
    {:ok, _calendar} = Calendars.create_calendar()
    conn = get conn, calendar_path(conn, :show, Ecto.UUID.generate)
    body = json_response(conn, 404)
    assert body["error"]
  end

  test "PUT ignores attempt to set the id", %{conn: conn} do
    {:ok, calendar} = Calendars.create_calendar()

    id = Ecto.UUID.generate
    conn = put conn, calendar_path(conn, :update, calendar), %{id: id}
    assert json_response(conn, 200)["id"] == calendar.id
  end

  test "PUT allows setting :title and :description", %{conn: conn} do
    {:ok, calendar} = Calendars.create_calendar()

    attrs = %{title: "new title", description: "new description"}
    conn = put conn, calendar_path(conn, :update, calendar), attrs
    body = json_response(conn, 200)

    assert body["title"] == "new title"
    assert body["description"] == "new description"
  end

  test "PUT returns an error if any attributes are invalid", %{conn: conn} do
    {:ok, calendar} = Calendars.create_calendar()

    attrs = %{title: String.duplicate("x", 101)}
    conn = put conn, calendar_path(conn, :update, calendar), attrs
    body = json_response(conn, 422)
    error = List.first(body["errors"])

    assert error["path"] == "#/title"
    assert error["code"] == "too_long"
  end

end

defmodule CalgyApi.CalendarControllerTest do
  use CalgyApi.ConnCase

  alias Plug.Conn
  alias Calgy.Calendars.Calendar
  alias Calgy.Repo

  setup %{conn: conn} do
    conn =
      conn
      |> put_req_header("accept", "application/json")

    {:ok, %{conn: conn}}
  end

  test "POST creates a pending calendar by default", %{conn: conn} do
    conn = post conn, calendar_path(conn, :create), %{}
    body = json_response(conn, 201)

    calendar = Repo.get(Calendar, body["id"])
    assert calendar

    location = Conn.get_resp_header(conn, "location") |> List.first
    assert location == calendar_url(conn, :show, calendar.admin_id) <> ";admin"

    assert body["state"] == "pending"
    assert body["public_url"] == calendar_url(conn, :show, calendar.id)
  end

  test "POST ignores attempts to set the id", %{conn: conn} do
    id = Ecto.UUID.generate
    conn = post conn, calendar_path(conn, :create), %{id: id}
    assert json_response(conn, 201)["id"] != id
  end

  test "POST allows setting :title and :description", %{conn: conn} do
    attrs = %{title: "my title", description: "my description"}
    conn = post conn, calendar_path(conn, :create), attrs
    body = json_response(conn, 201)

    assert body["title"] == "my title"
    assert body["description"] == "my description"
  end

  test "POST response does not include nil values", %{conn: conn} do
    conn = post conn, calendar_path(conn, :create), %{}
    body = json_response(conn, 201)

    refute Map.has_key?(body, "title")
    refute Map.has_key?(body, "description")
  end

  test "POST returns an error if any attributes are invalid", %{conn: conn} do
    attrs = %{title: String.duplicate("x", 101)}
    conn = post conn, calendar_path(conn, :create), attrs
    body = json_response(conn, 422)
    error = List.first(body["errors"])

    assert error["path"] == "#/title"
    assert error["code"] == "too_long"
  end

  test "GET shows information about a calendar", %{conn: conn} do
    {:ok, calendar} = Repo.insert(%Calendar{})
    conn = get conn, calendar_path(conn, :show, calendar)
    body = json_response(conn, 200)

    assert body["id"]
    assert body["state"]
  end

  test "GET returns 404 if uuid is invalid", %{conn: conn} do
    {:ok, _calendar} = Repo.insert(%Calendar{})
    conn = get conn, calendar_path(conn, :show, "teapot")
    body = json_response(conn, 404)
    assert body["error"]
  end

  test "GET retuns 404 if calendar is not found", %{conn: conn} do
    {:ok, _calendar} = Repo.insert(%Calendar{})
    conn = get conn, calendar_path(conn, :show, Ecto.UUID.generate)
    body = json_response(conn, 404)
    assert body["error"]
  end

  test "PUT ignores attempt to set the id", %{conn: conn} do
    {:ok, calendar} = Repo.insert(%Calendar{})

    id = Ecto.UUID.generate
    conn = put conn, calendar_path(conn, :update, calendar), %{id: id}
    assert json_response(conn, 200)["id"] == calendar.id
  end

  test "PUT allows setting :title and :description", %{conn: conn} do
    {:ok, calendar} = Repo.insert(%Calendar{})

    attrs = %{title: "new title", description: "new description"}
    conn = put conn, calendar_path(conn, :update, calendar), attrs
    body = json_response(conn, 200)

    assert body["title"] == "new title"
    assert body["description"] == "new description"
  end

  test "PUT returns an error if any attributes are invalid", %{conn: conn} do
    {:ok, calendar} = Repo.insert(%Calendar{})

    attrs = %{title: String.duplicate("x", 101)}
    conn = put conn, calendar_path(conn, :update, calendar), attrs
    body = json_response(conn, 422)
    error = List.first(body["errors"])

    assert error["path"] == "#/title"
    assert error["code"] == "too_long"
  end

end


Fix Elixir warnings

Fix Elixir warnings

defmodule Maxwell.Conn do
  @moduledoc """
  The Maxwell connection.

  This module defines a `Maxwell.Conn` struct and the main functions
  for working with Maxwell connections.

  ### Request fields

  These fields contain request information:

     * `url` - the requested url as a binary, example: `"www.example.com:8080/path/?foo=bar"`.
     * `method` - the request method as a atom, example: `GET`.
     * `req_headers` - the request headers as a map, example: `%{"content-type" => "text/plain"}`.
     * `req_body` - the request body, by default is an empty string. It is set
        to nil after the request is set.

  ### Response fields

  These fields contain response information:

     * `status` - the response status
     * `resp_headers` - the response headers as a map.
     * `resp_body` - the response body (todo desc).

  ### Connection fields

     * `state` - the connection state

  The connection state is used to track the connection lifecycle. It starts
  as `:unsent` but is changed to `:sending`, Its final result is `:sent` or `:error`.

  ### Protocols

  `Maxwell.Conn` implements Inspect protocols out of the box.
  The inspect protocol provides a nice representation of the connection.

  """
  @type t :: %__MODULE__{
    state: :unsent | :sending | :sent | :error,
    method: Atom.t,
    url: String.t,
    path: String.t,
    query_string: Map.t,
    opts: Keyword.t,
    req_headers: %{binary => binary},
    req_body: iodata | Map.t,
    status: non_neg_integer | nil,
    resp_headers: %{binary => binary},
    resp_body: iodata | Map.t,
    private: Map.t
  }

  defstruct state: :unsent,
    method: nil,
    url: "",
    path: "",
    query_string: %{},
    req_headers: %{},
    req_body: nil,
    opts: [],
    status: nil,
    resp_headers: %{},
    resp_body: "",
    private: %{}

  alias Maxwell.{Conn, Query}

  defmodule AlreadySentError do
    @moduledoc """
    Error raised when trying to modify or send an already sent request
    """
    defexception message: "the request was already sent"
  end

  defmodule NotSentError do
    @moduledoc """
    Error raised when no request is sent in a connection
    """
    defexception message: "the request was not sent yet"
  end

  @doc """
  Create a new connection.
  The url provided will be parsed by `URI.parse/1`, and the relevant connection fields will
  be set accordingly.

  ### Examples

      iex> new()
      %Maxwell.Conn{}

      iex> new("http://example.com/foo")
      %Maxwell.Conn{url: "http://example.com", path: "/foo"}

      iex> new("http://example.com/foo?bar=qux")
      %Maxwell.Conn{url: "http://example.com", path: "/foo", query_string: %{"bar" => "qux"}}
  """
  def new(), do: %Conn{}
  def new(url) when is_binary(url) do
    %URI{scheme: scheme, path: path, query: query} = uri = URI.parse(url)
    scheme = scheme || "http"
    path   = path || ""
    conn = case uri do
             %URI{host: nil} ->
               # This is a badly formed URI, so we'll do best effort:
               cond do
                 # example.com:8080
                 scheme != nil and Integer.parse(path) != :error ->
                   %Conn{url: "http://#{scheme}:#{path}"}
                 String.contains?(path, ".") -> # example.com
                   %Conn{url: "#{scheme}://#{path}"}
                 path == "localhost" -> # special case for localhost
                   %Conn{url: "#{scheme}://localhost"}
                 String.starts_with?(path, "/") -> # /example - not a valid hostname, assume it's a path
                   %Conn{path: path}
                 true -> # example - not a valid hostname, assume it's a path
                   %Conn{path: "/" <> path}
               end
             %URI{userinfo: nil, scheme: "http", port: 80, host: host} ->
               %Conn{url: "http://#{host}", path: path}
             %URI{userinfo: nil, scheme: "https", port: 443, host: host} ->
               %Conn{url: "https://#{host}", path: path}
             %URI{userinfo: nil, port: port, host: host} ->
               %Conn{url: "#{scheme}://#{host}:#{port}", path: path}
             %URI{userinfo: userinfo, port: port, host: host} ->
               %Conn{url: "#{scheme}://#{userinfo}@#{host}:#{port}", path: path}
           end
    case is_nil(query) do
      true   -> conn
      false -> put_query_string(conn, Query.decode(query))
    end
  end

  @doc """
  Set the path of the request.

  ### Examples

       iex> put_path(new(), "delete")
       %Maxwell.Conn{path: "delete"}
  """
  @spec put_path(Conn.t, String.t) :: Conn.t | no_return
  def put_path(%Conn{state: :unsent} = conn, path), do: %{conn | path: path}
  def put_path(_conn, _path), do: raise AlreadySentError

  @doc false
  def put_path(path) when is_binary(path) do
    IO.warn "put_path/1 is deprecated, use new/1 or new/2 followed by put_path/2 instead"
    put_path(new(), path)
  end

  @doc """
  Add query string to `conn.query_string`.

    * `conn` - `%Conn{}`
    * `query_map` - as map, for example `%{foo => bar}`

  ### Examples

      # %Conn{query_string: %{name: "zhong wen"}}
      put_query_string(%Conn{}, %{name: "zhong wen"})

  """
  @spec put_query_string(Conn.t, map()) :: Conn.t | no_return
  def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, query) do
    %{conn | query_string: Map.merge(qs, query)}
  end
  def put_query_string(_conn, _query_map), do: raise AlreadySentError

  @doc false
  def put_query_string(query) when is_map(query) do
    IO.warn "put_query_string/1 is deprecated, use new/1 or new/2 followed by put_query_string/2 instead"
    put_query_string(new(), query)
  end

  @doc """
  Set a query string value for the request.

  ### Examples

        iex> put_query_string(new(), :name, "zhong wen")
        %Maxwell.Conn{query_string: %{:name => "zhong wen"}}
  """
  def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, key, value) do
    %{conn | query_string: Map.put(qs, key, value)}
  end
  def put_query_string(_conn, _key, _value), do: raise AlreadySentError

  @doc """
  Merge a map of headers into the existing headers of the connection.

  ### Examples

      iex> %Maxwell.Conn{headers: %{"content-type" => "text/javascript"}
      |> put_req_headers(%{"Accept" => "application/json"})
      %Maxwell.Conn{req_headers: %{"accept" => "application/json", "content-type" => "text/javascript"}}
  """
  @spec put_req_headers(Conn.t, map()) :: Conn.t | no_return
  def put_req_headers(%Conn{state: :unsent, req_headers: headers} = conn, extra_headers) when is_map(extra_headers) do
    new_headers =
    extra_headers
    |> Enum.reduce(headers, fn {header_name, header_value}, acc ->
      Map.put(acc, String.downcase(header_name), header_value)
    end)
    %{conn | req_headers: new_headers}
  end
  def put_req_headers(_conn, _headers), do: raise AlreadySentError

  # TODO: Remove
  @doc false
  def put_req_header(headers) do
    IO.warn "put_req_header/1 is deprecated, use new/1 or new/2 followed by put_req_headers/2 instead"
    put_req_headers(new(), headers)
  end

  # TODO: Remove
  @doc false
  def put_req_header(conn, headers) when is_map(headers) do
    IO.warn "put_req_header/2 is deprecated, use put_req_headers/1 instead"
    put_req_headers(conn, headers)
  end

  @doc """
  Set a request header. If it already exists, it is updated.

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"content-type" => "text/javascript"}}
      |> put_req_header("Content-Type", "application/json")
      |> put_req_header("User-Agent", "zhongwencool")
      %Maxwell.Conn{req_headers: %{"content-type" => "application/json", "user-agent" => "zhongwenool"}
  """
  def put_req_header(%Conn{state: :unsent, req_headers: headers} = conn, key, value) do
    new_headers = Map.put(headers, String.downcase(key), value)
    %{conn | req_headers: new_headers}
  end
  def put_req_header(_conn, _key, _value), do: raise AlreadySentError

  @doc """
  Get all request headers as a map

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header
      %{"cookie" => "xyz"}
  """
  @spec get_req_header(Conn.t) :: %{String.t => String.t}
  def get_req_headers(%Conn{req_headers: headers}), do: headers

  # TODO: Remove
  @doc false
  def get_req_header(conn) do
    IO.warn "get_req_header/1 is deprecated, use get_req_headers/1 instead"
    get_req_headers(conn)
  end

  @doc """
  Get a request header by key. The key lookup is case-insensitive.
  Returns the value as a string, or nil if it doesn't exist.

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header("cookie")
      "xyz"
  """
  @spec get_req_header(Conn.t, String.t) :: String.t | nil
  def get_req_header(conn, nil) do
    IO.warn "get_req_header/2 with a nil key is deprecated, use get_req_headers/2 instead"
    get_req_headers(conn)
  end
  def get_req_header(%Conn{req_headers: headers}, key), do: Map.get(headers, String.downcase(key))

  @doc """
  Set adapter options for the request.

  ### Examples

      iex> put_options(new(), connect_timeout: 4000)
      %Maxwell.Conn{opts: [connect_timeout: 4000]}
  """
  @spec put_options(Conn.t, Keyword.t) :: Conn.t | no_return
  def put_options(%Conn{state: :unsent, opts: opts} = conn, extra_opts) when is_list(extra_opts) do
    %{conn | opts: Keyword.merge(opts, extra_opts)}
  end
  def put_options(_conn, extra_opts) when is_list(extra_opts), do: raise AlreadySentError

  @doc """
  Set an adapter option for the request.

  ### Examples

      iex> put_option(new(), :connect_timeout, 5000)
      %Maxwell.Conn{opts: [connect_timeout: 5000]}
  """
  @spec put_option(Conn.t, atom(), term()) :: Conn.t | no_return
  def put_option(%Conn{state: :unsent, opts: opts} = conn, key, value) when is_atom(key) do
    %{conn | opts: [{key, value} | opts]}
  end
  def put_option(%Conn{}, key, _value) when is_atom(key), do: raise AlreadySentError

  # TODO: remove
  @doc false
  def put_option(opts) when is_list(opts) do
    IO.warn "put_option/1 is deprecated, use new/1 or new/2 followed by put_options/2 instead"
    put_options(new(), opts)
  end

  # TODO: remove
  @doc false
  def put_option(conn, opts) when is_list(opts) do
    IO.warn "put_option/2 is deprecated, use put_options/2 instead"
    put_options(conn, opts)
  end

  @doc """
  Set the request body.

  ### Examples

      iex> put_req_body(new(), "new body")
      %Maxwell.Conn{req_body: "new_body"}
  """
  @spec put_req_body(Conn.t, Stream.t | binary()) :: Conn.t | no_return
  def put_req_body(%Conn{state: :unsent} = conn, req_body) do
    %{conn | req_body: req_body}
  end
  def put_req_body(_conn, _req_body), do: raise AlreadySentError

  # TODO: remove
  @doc false
  def put_req_body(body) do
    IO.warn "put_req_body/1 is deprecated, use new/1 or new/2 followed by put_req_body/2 instead"
    put_req_body(new(), body)
  end

  @doc """
  Get response status.
  Raises `Maxwell.Conn.NotSentError` when the request is unsent.

  ### Examples

      iex> get_status(%Maxwell.Conn{status: 200})
      200
  """
  @spec get_status(Conn.t) :: pos_integer | no_return
  def get_status(%Conn{status: status, state: state}) when state !== :unsent, do: status
  def get_status(_conn), do: raise NotSentError

  @doc """
  Get all response headers as a map.

  ### Examples

      iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"} |> get_resp_header
      %{"cookie" => "xyz"}
  """
  @spec get_resp_headers(Conn.t) :: %{String.t => String.t} | no_return
  def get_resp_headers(%Conn{state: :unsent}), do: raise NotSentError
  def get_resp_headers(%Conn{resp_headers: headers}), do: headers

  # TODO: remove
  @doc false
  def get_resp_header(conn) do
    IO.warn "get_resp_header/1 is deprecated, use get_resp_headers/1 instead"
    get_resp_headers(conn)
  end

  @doc """
  Get a response header by key.
  The value is returned as a string, or nil if the header is not set.

  ### Examples

      iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"}} |> get_resp_header("cookie")
      "xyz"
  """
  @spec get_resp_header(Conn.t, String.t) :: String.t | nil | no_return
  def get_resp_header(%Conn{state: :unsent}, _key), do: raise NotSentError
  # TODO: remove
  def get_resp_header(conn, nil) do
    IO.warn "get_resp_header/2 with a nil key is deprecated, use get_resp_headers/1 instead"
    get_resp_headers(conn)
  end
  def get_resp_header(%Conn{resp_headers: headers}, key), do: Map.get(headers, String.downcase(key))

  @doc """
  Return the response body.

  ### Examples

      iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "best http client"})
      "best http client"
  """
  @spec get_resp_body(Conn.t) :: binary() | map() | no_return
  def get_resp_body(%Conn{state: :sent, resp_body: body}), do: body
  def get_resp_body(_conn), do: raise NotSentError

  @doc """
  Return a value from the response body by key or with a parsing function.

  ### Examples

      iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: %{"name" => "xyz"}}, "name")
      "xyz"

      iex> func = fn(x) ->
      ...>   [key, value] = String.split(x, ":")
      ...>   value
      ...> end
      ...> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "name:xyz"}, func)
      "xyz"
  """
  def get_resp_body(%Conn{state: state}, _) when state != :sent,             do: raise NotSentError
  def get_resp_body(%Conn{resp_body: body}, func) when is_function(func, 1), do: func.(body)
  def get_resp_body(%Conn{resp_body: body}, keys) when is_list(keys),        do: get_in(body, keys)
  def get_resp_body(%Conn{resp_body: body}, key), do: body[key]

  @doc """
  Set a private value. If it already exists, it is updated.

  ### Examples

      iex> %Maxwell.Conn{private: %{}}
      |> put_private(:user_id, "zhongwencool")
      %Maxwell.Conn{private: %{user_id: "zhongwencool"}}
  """
  @spec put_private(Conn.t, Atom.t, term()) :: Conn.t
  def put_private(%Conn{private: private} = conn, key, value) do
    new_private = Map.put(private, key, value)
    %{conn | private: new_private}
  end

  @doc """
  Get a private value

  ### Examples

      iex> %Maxwell.Conn{private: %{user_id: "zhongwencool"}}
      |> get_private(:user_id)
      "zhongwencool"
  """
  @spec get_private(Conn.t, Atom.t) :: term()
  def get_private(%Conn{private: private}, key) do
    Map.get(private, key)
  end

  defimpl Inspect, for: Conn do
    def inspect(conn, opts) do
      Inspect.Any.inspect(conn, opts)
    end
  end
end

defmodule Maxwell.Conn do
  @moduledoc """
  The Maxwell connection.

  This module defines a `Maxwell.Conn` struct and the main functions
  for working with Maxwell connections.

  ### Request fields

  These fields contain request information:

     * `url` - the requested url as a binary, example: `"www.example.com:8080/path/?foo=bar"`.
     * `method` - the request method as a atom, example: `GET`.
     * `req_headers` - the request headers as a map, example: `%{"content-type" => "text/plain"}`.
     * `req_body` - the request body, by default is an empty string. It is set
        to nil after the request is set.

  ### Response fields

  These fields contain response information:

     * `status` - the response status
     * `resp_headers` - the response headers as a map.
     * `resp_body` - the response body (todo desc).

  ### Connection fields

     * `state` - the connection state

  The connection state is used to track the connection lifecycle. It starts
  as `:unsent` but is changed to `:sending`, Its final result is `:sent` or `:error`.

  ### Protocols

  `Maxwell.Conn` implements Inspect protocols out of the box.
  The inspect protocol provides a nice representation of the connection.

  """
  @type t :: %__MODULE__{
    state: :unsent | :sending | :sent | :error,
    method: Atom.t,
    url: String.t,
    path: String.t,
    query_string: Map.t,
    opts: Keyword.t,
    req_headers: %{binary => binary},
    req_body: iodata | Map.t,
    status: non_neg_integer | nil,
    resp_headers: %{binary => binary},
    resp_body: iodata | Map.t,
    private: Map.t
  }

  defstruct state: :unsent,
    method: nil,
    url: "",
    path: "",
    query_string: %{},
    req_headers: %{},
    req_body: nil,
    opts: [],
    status: nil,
    resp_headers: %{},
    resp_body: "",
    private: %{}

  alias Maxwell.{Conn, Query}

  defmodule AlreadySentError do
    @moduledoc """
    Error raised when trying to modify or send an already sent request
    """
    defexception message: "the request was already sent"
  end

  defmodule NotSentError do
    @moduledoc """
    Error raised when no request is sent in a connection
    """
    defexception message: "the request was not sent yet"
  end

  @doc """
  Create a new connection.
  The url provided will be parsed by `URI.parse/1`, and the relevant connection fields will
  be set accordingly.

  ### Examples

      iex> new()
      %Maxwell.Conn{}

      iex> new("http://example.com/foo")
      %Maxwell.Conn{url: "http://example.com", path: "/foo"}

      iex> new("http://example.com/foo?bar=qux")
      %Maxwell.Conn{url: "http://example.com", path: "/foo", query_string: %{"bar" => "qux"}}
  """
  def new(), do: %Conn{}
  def new(url) when is_binary(url) do
    %URI{scheme: scheme, path: path, query: query} = uri = URI.parse(url)
    scheme = scheme || "http"
    path   = path || ""
    conn = case uri do
             %URI{host: nil} ->
               # This is a badly formed URI, so we'll do best effort:
               cond do
                 # example.com:8080
                 scheme != nil and Integer.parse(path) != :error ->
                   %Conn{url: "http://#{scheme}:#{path}"}
                 String.contains?(path, ".") -> # example.com
                   %Conn{url: "#{scheme}://#{path}"}
                 path == "localhost" -> # special case for localhost
                   %Conn{url: "#{scheme}://localhost"}
                 String.starts_with?(path, "/") -> # /example - not a valid hostname, assume it's a path
                   %Conn{path: path}
                 true -> # example - not a valid hostname, assume it's a path
                   %Conn{path: "/" <> path}
               end
             %URI{userinfo: nil, scheme: "http", port: 80, host: host} ->
               %Conn{url: "http://#{host}", path: path}
             %URI{userinfo: nil, scheme: "https", port: 443, host: host} ->
               %Conn{url: "https://#{host}", path: path}
             %URI{userinfo: nil, port: port, host: host} ->
               %Conn{url: "#{scheme}://#{host}:#{port}", path: path}
             %URI{userinfo: userinfo, port: port, host: host} ->
               %Conn{url: "#{scheme}://#{userinfo}@#{host}:#{port}", path: path}
           end
    case is_nil(query) do
      true   -> conn
      false -> put_query_string(conn, Query.decode(query))
    end
  end

  @doc """
  Set the path of the request.

  ### Examples

       iex> put_path(new(), "delete")
       %Maxwell.Conn{path: "delete"}
  """
  @spec put_path(Conn.t, String.t) :: Conn.t | no_return
  def put_path(%Conn{state: :unsent} = conn, path), do: %{conn | path: path}
  def put_path(_conn, _path), do: raise AlreadySentError

  @doc false
  def put_path(path) when is_binary(path) do
    IO.warn "put_path/1 is deprecated, use new/1 or new/2 followed by put_path/2 instead"
    put_path(new(), path)
  end

  @doc """
  Add query string to `conn.query_string`.

    * `conn` - `%Conn{}`
    * `query_map` - as map, for example `%{foo => bar}`

  ### Examples

      # %Conn{query_string: %{name: "zhong wen"}}
      put_query_string(%Conn{}, %{name: "zhong wen"})

  """
  @spec put_query_string(Conn.t, map()) :: Conn.t | no_return
  def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, query) do
    %{conn | query_string: Map.merge(qs, query)}
  end
  def put_query_string(_conn, _query_map), do: raise AlreadySentError

  @doc false
  def put_query_string(query) when is_map(query) do
    IO.warn "put_query_string/1 is deprecated, use new/1 or new/2 followed by put_query_string/2 instead"
    put_query_string(new(), query)
  end

  @doc """
  Set a query string value for the request.

  ### Examples

        iex> put_query_string(new(), :name, "zhong wen")
        %Maxwell.Conn{query_string: %{:name => "zhong wen"}}
  """
  def put_query_string(%Conn{state: :unsent, query_string: qs} = conn, key, value) do
    %{conn | query_string: Map.put(qs, key, value)}
  end
  def put_query_string(_conn, _key, _value), do: raise AlreadySentError

  @doc """
  Merge a map of headers into the existing headers of the connection.

  ### Examples

      iex> %Maxwell.Conn{headers: %{"content-type" => "text/javascript"}
      |> put_req_headers(%{"Accept" => "application/json"})
      %Maxwell.Conn{req_headers: %{"accept" => "application/json", "content-type" => "text/javascript"}}
  """
  @spec put_req_headers(Conn.t, map()) :: Conn.t | no_return
  def put_req_headers(%Conn{state: :unsent, req_headers: headers} = conn, extra_headers) when is_map(extra_headers) do
    new_headers =
    extra_headers
    |> Enum.reduce(headers, fn {header_name, header_value}, acc ->
      Map.put(acc, String.downcase(header_name), header_value)
    end)
    %{conn | req_headers: new_headers}
  end
  def put_req_headers(_conn, _headers), do: raise AlreadySentError

  # TODO: Remove
  @doc false
  def put_req_header(headers) do
    IO.warn "put_req_header/1 is deprecated, use new/1 or new/2 followed by put_req_headers/2 instead"
    put_req_headers(new(), headers)
  end

  # TODO: Remove
  @doc false
  def put_req_header(conn, headers) when is_map(headers) do
    IO.warn "put_req_header/2 is deprecated, use put_req_headers/1 instead"
    put_req_headers(conn, headers)
  end

  @doc """
  Set a request header. If it already exists, it is updated.

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"content-type" => "text/javascript"}}
      |> put_req_header("Content-Type", "application/json")
      |> put_req_header("User-Agent", "zhongwencool")
      %Maxwell.Conn{req_headers: %{"content-type" => "application/json", "user-agent" => "zhongwenool"}
  """
  def put_req_header(%Conn{state: :unsent, req_headers: headers} = conn, key, value) do
    new_headers = Map.put(headers, String.downcase(key), value)
    %{conn | req_headers: new_headers}
  end
  def put_req_header(_conn, _key, _value), do: raise AlreadySentError

  @doc """
  Get all request headers as a map

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header
      %{"cookie" => "xyz"}
  """
  @spec get_req_header(Conn.t) :: %{String.t => String.t}
  def get_req_headers(%Conn{req_headers: headers}), do: headers

  # TODO: Remove
  @doc false
  def get_req_header(conn) do
    IO.warn "get_req_header/1 is deprecated, use get_req_headers/1 instead"
    get_req_headers(conn)
  end

  @doc """
  Get a request header by key. The key lookup is case-insensitive.
  Returns the value as a string, or nil if it doesn't exist.

  ### Examples

      iex> %Maxwell.Conn{req_headers: %{"cookie" => "xyz"} |> get_req_header("cookie")
      "xyz"
  """
  @spec get_req_header(Conn.t, String.t) :: String.t | nil
  def get_req_header(conn, nil) do
    IO.warn "get_req_header/2 with a nil key is deprecated, use get_req_headers/2 instead"
    get_req_headers(conn)
  end
  def get_req_header(%Conn{req_headers: headers}, key), do: Map.get(headers, String.downcase(key))

  @doc """
  Set adapter options for the request.

  ### Examples

      iex> put_options(new(), connect_timeout: 4000)
      %Maxwell.Conn{opts: [connect_timeout: 4000]}
  """
  @spec put_options(Conn.t, Keyword.t) :: Conn.t | no_return
  def put_options(%Conn{state: :unsent, opts: opts} = conn, extra_opts) when is_list(extra_opts) do
    %{conn | opts: Keyword.merge(opts, extra_opts)}
  end
  def put_options(_conn, extra_opts) when is_list(extra_opts), do: raise AlreadySentError

  @doc """
  Set an adapter option for the request.

  ### Examples

      iex> put_option(new(), :connect_timeout, 5000)
      %Maxwell.Conn{opts: [connect_timeout: 5000]}
  """
  @spec put_option(Conn.t, atom(), term()) :: Conn.t | no_return
  def put_option(%Conn{state: :unsent, opts: opts} = conn, key, value) when is_atom(key) do
    %{conn | opts: [{key, value} | opts]}
  end
  def put_option(%Conn{}, key, _value) when is_atom(key), do: raise AlreadySentError

  # TODO: remove
  @doc false
  def put_option(opts) when is_list(opts) do
    IO.warn "put_option/1 is deprecated, use new/1 or new/2 followed by put_options/2 instead"
    put_options(new(), opts)
  end

  # TODO: remove
  @doc false
  def put_option(conn, opts) when is_list(opts) do
    IO.warn "put_option/2 is deprecated, use put_options/2 instead"
    put_options(conn, opts)
  end

  @doc """
  Set the request body.

  ### Examples

      iex> put_req_body(new(), "new body")
      %Maxwell.Conn{req_body: "new_body"}
  """
  @spec put_req_body(Conn.t, Stream.t | binary()) :: Conn.t | no_return
  def put_req_body(%Conn{state: :unsent} = conn, req_body) do
    %{conn | req_body: req_body}
  end
  def put_req_body(_conn, _req_body), do: raise AlreadySentError

  # TODO: remove
  @doc false
  def put_req_body(body) do
    IO.warn "put_req_body/1 is deprecated, use new/1 or new/2 followed by put_req_body/2 instead"
    put_req_body(new(), body)
  end

  @doc """
  Get response status.
  Raises `Maxwell.Conn.NotSentError` when the request is unsent.

  ### Examples

      iex> get_status(%Maxwell.Conn{status: 200})
      200
  """
  @spec get_status(Conn.t) :: pos_integer | no_return
  def get_status(%Conn{status: status, state: state}) when state !== :unsent, do: status
  def get_status(_conn), do: raise NotSentError

  @doc """
  Get all response headers as a map.

  ### Examples

      iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"} |> get_resp_header
      %{"cookie" => "xyz"}
  """
  @spec get_resp_headers(Conn.t) :: %{String.t => String.t} | no_return
  def get_resp_headers(%Conn{state: :unsent}), do: raise NotSentError
  def get_resp_headers(%Conn{resp_headers: headers}), do: headers

  # TODO: remove
  @doc false
  def get_resp_header(conn) do
    IO.warn "get_resp_header/1 is deprecated, use get_resp_headers/1 instead"
    get_resp_headers(conn)
  end

  @doc """
  Get a response header by key.
  The value is returned as a string, or nil if the header is not set.

  ### Examples

      iex> %Maxwell.Conn{resp_headers: %{"cookie" => "xyz"}} |> get_resp_header("cookie")
      "xyz"
  """
  @spec get_resp_header(Conn.t, String.t) :: String.t | nil | no_return
  def get_resp_header(%Conn{state: :unsent}, _key), do: raise NotSentError
  # TODO: remove
  def get_resp_header(conn, nil) do
    IO.warn "get_resp_header/2 with a nil key is deprecated, use get_resp_headers/1 instead"
    get_resp_headers(conn)
  end
  def get_resp_header(%Conn{resp_headers: headers}, key), do: Map.get(headers, String.downcase(key))

  @doc """
  Return the response body.

  ### Examples

      iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "best http client"})
      "best http client"
  """
  @spec get_resp_body(Conn.t) :: binary() | map() | no_return
  def get_resp_body(%Conn{state: :sent, resp_body: body}), do: body
  def get_resp_body(_conn), do: raise NotSentError

  @doc """
  Return a value from the response body by key or with a parsing function.

  ### Examples

      iex> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: %{"name" => "xyz"}}, "name")
      "xyz"

      iex> func = fn(x) ->
      ...>   [key, value] = String.split(x, ":")
      ...>   value
      ...> end
      ...> get_resp_body(%Maxwell.Conn{state: :sent, resp_body: "name:xyz"}, func)
      "xyz"
  """
  def get_resp_body(%Conn{state: state}, _) when state != :sent,             do: raise NotSentError
  def get_resp_body(%Conn{resp_body: body}, func) when is_function(func, 1), do: func.(body)
  def get_resp_body(%Conn{resp_body: body}, keys) when is_list(keys),        do: get_in(body, keys)
  def get_resp_body(%Conn{resp_body: body}, key), do: body[key]

  @doc """
  Set a private value. If it already exists, it is updated.

  ### Examples

      iex> %Maxwell.Conn{private: %{}}
      |> put_private(:user_id, "zhongwencool")
      %Maxwell.Conn{private: %{user_id: "zhongwencool"}}
  """
  @spec put_private(Conn.t, Atom.t, term()) :: Conn.t
  def put_private(%Conn{private: private} = conn, key, value) do
    new_private = Map.put(private, key, value)
    %{conn | private: new_private}
  end

  @doc """
  Get a private value

  ### Examples

      iex> %Maxwell.Conn{private: %{user_id: "zhongwencool"}}
      |> get_private(:user_id)
      "zhongwencool"
  """
  @spec get_private(Conn.t, Atom.t) :: term()
  def get_private(%Conn{private: private} = conn, key) do
    Map.get(private, key)
  end

  defimpl Inspect, for: Conn do
    def inspect(conn, opts) do
      Inspect.Any.inspect(conn, opts)
    end
  end
end


Improve reader error details

Improve reader error details

defmodule Geolix.Adapter.MMDB2.Reader do
  @moduledoc false

  @doc """
  Reads a database file and returns the data and metadata parts from it.
  """
  @spec read_database(String.t()) :: MMDB2Decoder.parse_result()
  def read_database("http" <> _ = filename) do
    {:ok, _} = Application.ensure_all_started(:inets)

    case :httpc.request(String.to_charlist(filename)) do
      {:ok, {{_, 200, _}, _, body}} ->
        body
        |> IO.iodata_to_binary()
        |> maybe_gunzip(filename)
        |> maybe_untar(filename)
        |> MMDB2Decoder.parse_database()

      {:error, err} ->
        {:error, {:remote, err}}
    end
  end

  def read_database(nil), do: {:error, :enoent}

  def read_database(filename) do
    case File.stat(filename) do
      {:ok, _} ->
        filename
        |> File.read!()
        |> maybe_gunzip(filename)
        |> maybe_untar(filename)
        |> MMDB2Decoder.parse_database()

      error ->
        error
    end
  end

  defp find_mmdb_contents([]), do: nil

  defp find_mmdb_contents([{file, contents} | files]) do
    if String.ends_with?(to_string(file), ".mmdb") do
      contents
    else
      find_mmdb_contents(files)
    end
  end

  defp maybe_untar(data, filename) do
    if String.ends_with?(filename, [".tar", ".tar.gz"]) do
      {:ok, files} = :erl_tar.extract({:binary, data}, [:memory])

      find_mmdb_contents(files)
    else
      data
    end
  end

  defp maybe_gunzip(data, filename) do
    if String.ends_with?(filename, ".gz") do
      :zlib.gunzip(data)
    else
      data
    end
  end
end

defmodule Geolix.Adapter.MMDB2.Reader do
  @moduledoc false

  @doc """
  Reads a database file and returns the data and metadata parts from it.
  """
  @spec read_database(String.t()) :: MMDB2Decoder.parse_result()
  def read_database("http" <> _ = filename) do
    {:ok, _} = Application.ensure_all_started(:inets)

    case :httpc.request(String.to_charlist(filename)) do
      {:ok, {{_, 200, _}, _, body}} ->
        body
        |> IO.iodata_to_binary()
        |> maybe_gunzip(filename)
        |> maybe_untar(filename)
        |> MMDB2Decoder.parse_database()

      {:error, err} ->
        {:error, {:remote, err}}
    end
  end

  def read_database(nil), do: {:error, :enoent}

  def read_database(filename) do
    if File.regular?(filename) do
      filename
      |> File.read!()
      |> maybe_gunzip(filename)
      |> maybe_untar(filename)
      |> MMDB2Decoder.parse_database()
    else
      {:error, :enoent}
    end
  end

  defp find_mmdb_contents([]), do: nil

  defp find_mmdb_contents([{file, contents} | files]) do
    if String.ends_with?(to_string(file), ".mmdb") do
      contents
    else
      find_mmdb_contents(files)
    end
  end

  defp maybe_untar(data, filename) do
    if String.ends_with?(filename, [".tar", ".tar.gz"]) do
      {:ok, files} = :erl_tar.extract({:binary, data}, [:memory])

      find_mmdb_contents(files)
    else
      data
    end
  end

  defp maybe_gunzip(data, filename) do
    if String.ends_with?(filename, ".gz") do
      :zlib.gunzip(data)
    else
      data
    end
  end
end


Add character limit to summary

Add character limit to summary

defmodule ElixirJobs.Repo.Migrations.AddSummaryToOffers do
  use Ecto.Migration

  def change do
    alter table(:offers) do
      add :summary, :string, size: 200
    end

  end
end

defmodule ElixirJobs.Repo.Migrations.AddSummaryToOffers do
  use Ecto.Migration

  def change do
    alter table(:offers) do
      add :summary, :string
    end

  end
end