Fixing wrong namespace on umbrella mixfile, also removing useless boilerplate.

defmodule HELM.Mixfile do
  use Mix.Project

  def project do
    [apps_path: "apps",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  defp deps do
    []
  end
end

defmodule Helm.Mixfile do
  use Mix.Project

  def project do
    [apps_path: "apps",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options.
  #
  # Dependencies listed here are available only for this project
  # and cannot be accessed from applications inside the apps folder
  defp deps do
    []
  end
end


update version

update version

defmodule Flambo.Mixfile do
  use Mix.Project

  def project do
    [app: :flambo,
     version: "0.4.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [mod: {Flambo, []},
     applications: [:logger, :slack]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [{:slack, "~> 0.6.0"},
     {:websocket_client, git: "https://github.com/jeremyong/websocket_client"}]
  end
end

defmodule Flambo.Mixfile do
  use Mix.Project

  def project do
    [app: :flambo,
     version: "0.1.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [mod: {Flambo, []},
     applications: [:logger, :slack]]
  end

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [{:slack, "~> 0.6.0"},
     {:websocket_client, git: "https://github.com/jeremyong/websocket_client"}]
  end
end


Remember to start honeybadger

Remember to start honeybadger

defmodule MrRebase.Mixfile do
  use Mix.Project

  def project do
    [app: :mr_rebase,
     version: "0.0.1",
     elixir: "~> 1.0",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix, :gettext] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     aliases: aliases,
     deps: deps]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {MrRebase, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :honeybadger, :logger, :gettext,
                    :phoenix_ecto, :postgrex, :oauth2, :tentacat]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.1.1"},
     {:phoenix_ecto, "~> 2.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.3"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.9"},
     {:cowboy, "~> 1.0"},
     {:oauth2, "~> 0.5"},
     {:tentacat, "~> 0.2"},
     {:secure_random, "~> 0.2"},
     {:honeybadger, "~> 0.3"}]
  end

  # Aliases are shortcut or tasks specific to the current project.
  # For example, to create, migrate and run the seeds file at once:
  #
  #     $ mix ecto.setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
     "ecto.reset": ["ecto.drop", "ecto.setup"]]
  end
end

defmodule MrRebase.Mixfile do
  use Mix.Project

  def project do
    [app: :mr_rebase,
     version: "0.0.1",
     elixir: "~> 1.0",
     elixirc_paths: elixirc_paths(Mix.env),
     compilers: [:phoenix, :gettext] ++ Mix.compilers,
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     aliases: aliases,
     deps: deps]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [mod: {MrRebase, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :logger, :gettext,
                    :phoenix_ecto, :postgrex, :oauth2, :tentacat]]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
  defp elixirc_paths(_),     do: ["lib", "web"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:phoenix, "~> 1.1.1"},
     {:phoenix_ecto, "~> 2.0"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.3"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:gettext, "~> 0.9"},
     {:cowboy, "~> 1.0"},
     {:oauth2, "~> 0.5"},
     {:tentacat, "~> 0.2"},
     {:secure_random, "~> 0.2"},
     {:honeybadger, "~> 0.3"}]
  end

  # Aliases are shortcut or tasks specific to the current project.
  # For example, to create, migrate and run the seeds file at once:
  #
  #     $ mix ecto.setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
     "ecto.reset": ["ecto.drop", "ecto.setup"]]
  end
end


Bump version to 0.5.1

Bump version to 0.5.1

defmodule TrackerBot.Mixfile do
  use Mix.Project

  def project do
    [
      app: :tracker_bot,
      version: "0.5.1",
      elixir: "~> 1.5",
      name: "Tracker Bot",
      homepage_url: "https://github.com/ne1ro/tracker_bot",
      build_embedded: Mix.env == :prod,
      start_permanent: Mix.env == :prod,
      escript: [main_module: TrackerBot.CLI],
      docs: [extras: ["README.md"], output: "./doc/app"],
      deps: deps(),
      aliases: aliases(),
      dialyzer: dialyzer()
    ]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [
      mod: {TrackerBot, []},
      extra_applications: ~w(logger cowboy plug)a,
      elixirc_paths: elixirc_paths(Mix.env)
    ]
  end

  # This makes sure your factory and any other modules in test/support are compiled
  # when in the test environment.
  defp elixirc_paths(:test), do: ~w(lib test/support)
  defp elixirc_paths(_), do: ~w(lib)

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
     {:distillery, "~> 1.4"},
     {:edeliver, "~> 1.4"},
     {:effects, "~> 0.1.0"},
     {:guardsafe, "~> 0.5.0"},
     {:monadex, "~> 1.0.2"},
     {:httpoison, "~> 0.11.0"},
     {:hackney, "~> 1.8"},
     {:poison, "~> 3.1"},
     {:nadia, "~> 0.4"},
     {:cowboy, "~> 1.0.0"},
     {:plug, "~> 1.3"},
     {:logger_file_backend, "~> 0.0.9"},
     {:timex, "~> 3.1"},
     {:credo, "~> 0.8", only: ~w(dev test)a},
     {:dialyxir, "~> 0.4", only: ~w(dev test)a, runtime: false},
     {:edeliver, ">= 1.2.9", only: :dev},
     {:eper, "~> 0.94.0", only: :dev},
     {:ex_machina, "~> 0.6.1", only: ~w(dev test)a},
     {:ex_doc, "~> 0.11", only: :dev},
     {:observer_cli, "~> 1.0.5", only: :dev},
     {:espec, "~> 1.1.0", only: :test},
     {:faker, "~> 0.5", only: :test,}
   ]
  end

  # Dialyzer's configuration
  def dialyzer, do: [
    plt_add_deps: :apps_direct,
    flags: ~w(-Wunmatched_returns -Werror_handling -Wrace_conditions -Wunderspecs
              -Wunknown -Woverspecs -Wspecdiffs)
  ]

  defp aliases do
    [
      build: "release",
      server: "run",
      quality: ["compile --warnings-as-errors --force", "credo --strict", "dialyzer"],
      setup: [
        "local.hex --force",
        "local.rebar --force",
        "deps.get",
        "compile"
      ]
    ]
  end
end

defmodule TrackerBot.Mixfile do
  use Mix.Project

  def project do
    [
      app: :tracker_bot,
      version: "0.5.0",
      elixir: "~> 1.5",
      name: "Tracker Bot",
      homepage_url: "https://github.com/ne1ro/tracker_bot",
      build_embedded: Mix.env == :prod,
      start_permanent: Mix.env == :prod,
      escript: [main_module: TrackerBot.CLI],
      docs: [extras: ["README.md"], output: "./doc/app"],
      deps: deps(),
      aliases: aliases(),
      dialyzer: dialyzer()
    ]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [
      mod: {TrackerBot, []},
      extra_applications: ~w(logger cowboy plug)a,
      elixirc_paths: elixirc_paths(Mix.env)
    ]
  end

  # This makes sure your factory and any other modules in test/support are compiled
  # when in the test environment.
  defp elixirc_paths(:test), do: ~w(lib test/support)
  defp elixirc_paths(_), do: ~w(lib)

  # Dependencies can be Hex packages:
  #
  #   {:mydep, "~> 0.3.0"}
  #
  # Or git/path repositories:
  #
  #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
  #
  # Type "mix help deps" for more examples and options
  defp deps do
    [
     {:distillery, "~> 1.4"},
     {:edeliver, "~> 1.4"},
     {:effects, "~> 0.1.0"},
     {:guardsafe, "~> 0.5.0"},
     {:monadex, "~> 1.0.2"},
     {:httpoison, "~> 0.11.0"},
     {:hackney, "~> 1.8"},
     {:poison, "~> 3.1"},
     {:nadia, "~> 0.4"},
     {:cowboy, "~> 1.0.0"},
     {:plug, "~> 1.3"},
     {:logger_file_backend, "~> 0.0.9"},
     {:timex, "~> 3.1"},
     {:credo, "~> 0.8", only: ~w(dev test)a},
     {:dialyxir, "~> 0.4", only: ~w(dev test)a, runtime: false},
     {:edeliver, ">= 1.2.9", only: :dev},
     {:eper, "~> 0.94.0", only: :dev},
     {:ex_machina, "~> 0.6.1", only: ~w(dev test)a},
     {:ex_doc, "~> 0.11", only: :dev},
     {:observer_cli, "~> 1.0.5", only: :dev},
     {:espec, "~> 1.1.0", only: :test},
     {:faker, "~> 0.5", only: :test,}
   ]
  end

  # Dialyzer's configuration
  def dialyzer, do: [
    plt_add_deps: :apps_direct,
    flags: ~w(-Wunmatched_returns -Werror_handling -Wrace_conditions -Wunderspecs
              -Wunknown -Woverspecs -Wspecdiffs)
  ]

  defp aliases do
    [
      build: "release",
      server: "run",
      quality: ["compile --warnings-as-errors --force", "credo --strict", "dialyzer"],
      setup: [
        "local.hex --force",
        "local.rebar --force",
        "deps.get",
        "compile"
      ]
    ]
  end
end


Bump version to 1.6.0-rc.1

Bump version to 1.6.0-rc.1


Don't include elixir_make in the runtime

This also bumps elixir_make to 0.4 for consistency with most other
projects.

defmodule Nerves.Neopixel.Mixfile do
  use Mix.Project

  def project do
   [app: :nerves_neopixel,
    version: "0.3.1",
    description: "Drive WS2812B \"NeoPixel\" RGB LED strips from a Raspberry Pi using Elixir.",
    elixir: "~> 1.3",
    make_clean: ["clean"],
    compilers: [:elixir_make | Mix.compilers],
    build_embedded: Mix.env == :prod,
    start_permanent: Mix.env == :prod,
    package: package(),
    deps: deps()]
  end

  def application do
   [applications: [:logger]]
  end

  defp deps do
    [{:elixir_make, "~> 0.4", runtime: false}]
  end

  defp package do
   [files: ["lib", "src", "config", "mix.exs", "README*", "LICENSE*", "Makefile"],
    maintainers: ["Greg Mefford"],
    licenses: ["MIT", "BSD 2-Clause"],
    links: %{"GitHub" => "https://github.com/GregMefford/nerves_neopixel"}]
  end
end

defmodule Nerves.Neopixel.Mixfile do
  use Mix.Project

  def project do
   [app: :nerves_neopixel,
    version: "0.3.1",
    description: "Drive WS2812B \"NeoPixel\" RGB LED strips from a Raspberry Pi using Elixir.",
    elixir: "~> 1.3",
    make_clean: ["clean"],
    compilers: [:elixir_make | Mix.compilers],
    build_embedded: Mix.env == :prod,
    start_permanent: Mix.env == :prod,
    package: package(),
    deps: deps()]
  end

  def application do
   [applications: [:logger]]
  end

  defp deps do
    [{:elixir_make, "~> 0.3"}]
  end

  defp package do
   [files: ["lib", "src", "config", "mix.exs", "README*", "LICENSE*", "Makefile"],
    maintainers: ["Greg Mefford"],
    licenses: ["MIT", "BSD 2-Clause"],
    links: %{"GitHub" => "https://github.com/GregMefford/nerves_neopixel"}]
  end
end


convert FeatureStatusHandler state to struct

defmodule Iodized.Web.Admin.FeatureStatusHandler do

  @persistence Iodized.FeaturePersistence.Mnesia

  def init(_transport, _req, _opts) do
    {:upgrade, :protocol, :cowboy_rest}
  end

  def rest_init(req, _opts) do
    state = %{feature: nil}
    {:ok, req, state}
  end

  def allowed_methods(req, state) do
    {["GET", "PUT", "DELETE"], req, state}
  end

  def resource_exists(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    case @persistence.find_feature(feature_id) do
      :not_found ->     {false, req, state}
      {:ok, feature} -> state = %{state| feature: feature}
                        {true, req, state}
    end
  end

  def content_types_provided(req, state) do
    providers = [
      {{"application", "json", :*}, :render_feature}
    ]
    {providers, req, state}
  end

  def render_feature(req, state) do
    req = :cowboy_req.set_resp_header("content-type", "application/json; charset=utf-8", req)
    body = state.feature |> Iodized.Feature.json |> JSEX.encode!(indent: 2)
    {body, req, state}
  end

  def content_types_accepted(req, state) do
    acceptors = [
      {{"application", "json", :*}, :save_feature}
    ]
    {acceptors, req, state}
  end

  def save_feature(req, state) do
    {:ok, feature_json, req} = :cowboy_req.body(req)
    feature = feature_json |> JSEX.decode!(labels: :atom) |> Iodized.Feature.from_json
    @persistence.save_feature(feature)
    {true, req, state}
  end

  def delete_resource(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    @persistence.delete_feature(feature_id)
    {true, req, state}
  end
end

defmodule Iodized.Web.Admin.FeatureStatusHandler do

  @persistence Iodized.FeaturePersistence.Mnesia

  defrecord State, feature: nil

  def init(_transport, _req, _opts) do
    {:upgrade, :protocol, :cowboy_rest}
  end

  def rest_init(req, _opts) do
    state = State.new()
    {:ok, req, state}
  end

  def allowed_methods(req, state) do
    {["GET", "PUT", "DELETE"], req, state}
  end

  def resource_exists(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    case @persistence.find_feature(feature_id) do
      :not_found ->     {false, req, state}
      {:ok, feature} -> state = state.feature(feature)
                        {true, req, state}
    end
  end

  def content_types_provided(req, state) do
    providers = [
      {{"application", "json", :*}, :render_feature}
    ]
    {providers, req, state}
  end

  def render_feature(req, state) do
    req = :cowboy_req.set_resp_header("content-type", "application/json; charset=utf-8", req)
    body = state.feature |> Iodized.Feature.json |> JSEX.encode!(indent: 2)
    {body, req, state}
  end

  def content_types_accepted(req, state) do
    acceptors = [
      {{"application", "json", :*}, :save_feature}
    ]
    {acceptors, req, state}
  end

  def save_feature(req, state) do
    {:ok, feature_json, req} = :cowboy_req.body(req)
    feature = feature_json |> JSEX.decode!(labels: :atom) |> Iodized.Feature.from_json
    @persistence.save_feature(feature)
    {true, req, state}
  end

  def delete_resource(req, state) do
    {feature_id, req} = :cowboy_req.binding(:feature_id, req)
    @persistence.delete_feature(feature_id)
    {true, req, state}
  end
end


Remove debug printing from config provider

defmodule Mix.Releases.Config.Providers.Elixir do
  @moduledoc """
  Provides loading of Elixir's `config.exs` config file.
  """

  use Mix.Releases.Config.Provider

  alias Mix.Releases.Logger

  @impl Provider
  def init([path]) do
    with {:ok, path} <- Provider.expand_path(path) do
      path
      |> eval!()
      |> Mix.Config.persist()
    else
      {:error, reason} ->
        exit(reason)
    end
  end

  @doc """
  Given a path to a config file, this function will return the quoted AST of
  that config and all configs that it imports.
  """
  def read_quoted!(file) do
    if String.contains?(file, "*") do
      {quoted, _} = do_read_quoted_wildcard!(file, [])
      quoted
    else
      {quoted, _} = do_read_quoted!(file, [])
      quoted
    end
  end

  @doc false
  def eval!(path, imported_paths \\ [])

  Code.ensure_loaded(Mix.Config)
  if function_exported?(Mix.Config, :eval!, 2) do
    def eval!(path, imported_paths) do
      {config, _} = Mix.Config.eval!(path, imported_paths)
      config
    end
  else
    def eval!(path, imported_paths), do: Mix.Config.read!(path, imported_paths)
  end

  defp do_read_quoted_wildcard!(path, loaded_paths) do
    # This has a wildcard path, so we need to walk the list
    # of files, and strip the `use Mix.Config` from all but the first,
    # and merge all of the quoted contents of those files
    {final_quoted, new_loaded_paths} =
      path
      |> Path.wildcard()
      |> Enum.reduce({nil, loaded_paths}, fn
        f, {nil, loaded_paths} ->
          # Extract the quoted body of the top-level block for merging
          {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(f, loaded_paths)
          {quoted, new_loaded_paths}

        f, {quoted, loaded_paths} ->
          if f in loaded_paths do
            raise ArgumentError, message: "recursive load of #{f} detected"
          end

          # Again, extract the quoted body, strip the `use`, and concat to the
          # head of the merged quoted body
          {{:__block__, _, f_quoted}, new_loaded_paths} = do_read_quoted!(f, loaded_paths)

          f_quoted =
            f_quoted
            |> Enum.reject(fn
              {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
              _ -> false
            end)

          {Enum.concat(quoted, f_quoted), new_loaded_paths}
      end)

    # In the final step, reverse the quoted body so that they are in the file
    # in the order they were traversed, and wrap them all in a block
    {{:__block__, [], final_quoted}, new_loaded_paths}
  end

  defp do_read_quoted!(file, loaded_paths) do
    file = Path.expand(file)

    if file in loaded_paths do
      raise ArgumentError, message: "recursive load of #{file} detected"
    end

    content = File.read!(file)
    quoted = Code.string_to_quoted!(content, file: file, line: 1)
    merged = merge_imports(quoted, [], file, [file | loaded_paths])

    {merged, loaded_paths}
  end

  defp merge_imports({:__block__, _, block}, acc, file, loaded_paths) do
    merge_imports(block, acc, file, loaded_paths)
  end

  defp merge_imports(item, acc, file, loaded_paths) when is_tuple(item) do
    merge_imports([item], acc, file, loaded_paths)
  end

  defp merge_imports([], acc, _file, _loaded_paths) do
    {:__block__, [], Enum.reverse(acc)}
  end

  defp merge_imports([{:import_config, _, [path]} | block], acc, file, loaded_paths)
       when is_binary(path) do
    path = Path.join(Path.dirname(file), Path.relative_to(path, file))

    {quoted, new_loaded_paths} =
      if String.contains?(path, "*") do
        {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted_wildcard!(path, loaded_paths)
        {quoted, new_loaded_paths}
      else
        {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(path, loaded_paths)
        {quoted, new_loaded_paths}
      end

    new_acc =
      quoted
      |> Enum.reject(fn
        {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
        _ -> false
      end)
      |> Enum.reverse()
      |> Enum.concat(acc)

    merge_imports(block, new_acc, file, new_loaded_paths)
  end

  defp merge_imports([{:import_config, _, [path_expr]} | block], acc, file, loaded_paths) do
    case eval_path(acc, path_expr) do
      {:error, err} ->
        raise ArgumentError,
          "expected valid import_config path expression in #{file}: " <>
          "#{inspect err}"

      path ->
        path = Path.join(Path.dirname(file), Path.relative_to(path, file))

        {quoted, new_loaded_paths} =
          if String.contains?(path, "*") do
            {{:__block__, _, quoted}, new_loaded_paths} =
              do_read_quoted_wildcard!(path, loaded_paths)

            {quoted, new_loaded_paths}
          else
            {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(path, loaded_paths)
            {quoted, new_loaded_paths}
          end

        new_acc =
          quoted
          |> Enum.reject(fn
            {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
            _ -> false
          end)
          |> Enum.reverse()
          |> Enum.concat(acc)

        merge_imports(block, new_acc, file, new_loaded_paths)
    end
  end

  defp merge_imports([{:config, env, [:kernel | _]} = other | rest], acc, file, loaded_paths) do
    line = Keyword.get(env, :line, "N/A")
    file_path = Path.relative_to_cwd(file)

    Logger.warn(
      "Found config setting for :kernel application in Mix config!\n" <>
        "    File: #{file_path}\n" <>
        "    Line: #{line}\n" <>
        "    Any :kernel config settings need to be placed in vm.args, or they will not take effect!"
    )

    merge_imports(rest, [other | acc], file, loaded_paths)
  end

  defp merge_imports([other | rest], acc, file, loaded_paths) do
    merge_imports(rest, [other | acc], file, loaded_paths)
  end

  defp eval_path(_acc, path) when is_binary(path) do
    path
  end

  defp eval_path(acc, expr) do
    # Rebuild script context without Mix.Config macros
    stripped = strip_config_macros(acc, [expr])
    quoted = {:__block__, [], stripped}

    try do
      {path, _bindings} = Code.eval_quoted(quoted)
      path
    rescue
      e ->
        {:error, e}
    end
  end

  defp strip_config_macros([], acc), do: acc

  defp strip_config_macros([{:use, _, [{:__aliases__, _, [:Mix, :Config]}]} | rest], acc) do
    strip_config_macros(rest, acc)
  end

  defp strip_config_macros([{type, _, _} | rest], acc) when type in [:import_config, :config] do
    strip_config_macros(rest, acc)
  end

  defp strip_config_macros([expr | rest], acc) do
    strip_config_macros(rest, [expr | acc])
  end
end

defmodule Mix.Releases.Config.Providers.Elixir do
  @moduledoc """
  Provides loading of Elixir's `config.exs` config file.
  """

  use Mix.Releases.Config.Provider

  alias Mix.Releases.Logger

  @impl Provider
  def init([path]) do
    with {:ok, path} <- Provider.expand_path(path) do
      path
      |> IO.inspect(label: :path)
      |> eval!()
      |> IO.inspect(label: :eval)
      |> Mix.Config.persist()
    else
      {:error, reason} ->
        exit(reason)
    end
  end

  @doc """
  Given a path to a config file, this function will return the quoted AST of
  that config and all configs that it imports.
  """
  def read_quoted!(file) do
    if String.contains?(file, "*") do
      {quoted, _} = do_read_quoted_wildcard!(file, [])
      quoted
    else
      {quoted, _} = do_read_quoted!(file, [])
      quoted
    end
  end

  @doc false
  def eval!(path, imported_paths \\ [])

  Code.ensure_loaded(Mix.Config)
  if function_exported?(Mix.Config, :eval!, 2) do
    def eval!(path, imported_paths) do
      {config, _} = Mix.Config.eval!(path, imported_paths)
      config
    end
  else
    def eval!(path, imported_paths), do: Mix.Config.read!(path, imported_paths)
  end

  defp do_read_quoted_wildcard!(path, loaded_paths) do
    # This has a wildcard path, so we need to walk the list
    # of files, and strip the `use Mix.Config` from all but the first,
    # and merge all of the quoted contents of those files
    {final_quoted, new_loaded_paths} =
      path
      |> Path.wildcard()
      |> Enum.reduce({nil, loaded_paths}, fn
        f, {nil, loaded_paths} ->
          # Extract the quoted body of the top-level block for merging
          {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(f, loaded_paths)
          {quoted, new_loaded_paths}

        f, {quoted, loaded_paths} ->
          if f in loaded_paths do
            raise ArgumentError, message: "recursive load of #{f} detected"
          end

          # Again, extract the quoted body, strip the `use`, and concat to the
          # head of the merged quoted body
          {{:__block__, _, f_quoted}, new_loaded_paths} = do_read_quoted!(f, loaded_paths)

          f_quoted =
            f_quoted
            |> Enum.reject(fn
              {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
              _ -> false
            end)

          {Enum.concat(quoted, f_quoted), new_loaded_paths}
      end)

    # In the final step, reverse the quoted body so that they are in the file
    # in the order they were traversed, and wrap them all in a block
    {{:__block__, [], final_quoted}, new_loaded_paths}
  end

  defp do_read_quoted!(file, loaded_paths) do
    file = Path.expand(file)

    if file in loaded_paths do
      raise ArgumentError, message: "recursive load of #{file} detected"
    end

    content = File.read!(file)
    quoted = Code.string_to_quoted!(content, file: file, line: 1)
    merged = merge_imports(quoted, [], file, [file | loaded_paths])

    {merged, loaded_paths}
  end

  defp merge_imports({:__block__, _, block}, acc, file, loaded_paths) do
    merge_imports(block, acc, file, loaded_paths)
  end

  defp merge_imports(item, acc, file, loaded_paths) when is_tuple(item) do
    merge_imports([item], acc, file, loaded_paths)
  end

  defp merge_imports([], acc, _file, _loaded_paths) do
    {:__block__, [], Enum.reverse(acc)}
  end

  defp merge_imports([{:import_config, _, [path]} | block], acc, file, loaded_paths)
       when is_binary(path) do
    path = Path.join(Path.dirname(file), Path.relative_to(path, file))

    {quoted, new_loaded_paths} =
      if String.contains?(path, "*") do
        {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted_wildcard!(path, loaded_paths)
        {quoted, new_loaded_paths}
      else
        {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(path, loaded_paths)
        {quoted, new_loaded_paths}
      end

    new_acc =
      quoted
      |> Enum.reject(fn
        {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
        _ -> false
      end)
      |> Enum.reverse()
      |> Enum.concat(acc)

    merge_imports(block, new_acc, file, new_loaded_paths)
  end

  defp merge_imports([{:import_config, _, [path_expr]} | block], acc, file, loaded_paths) do
    case eval_path(acc, path_expr) do
      {:error, err} ->
        raise ArgumentError,
          "expected valid import_config path expression in #{file}: " <>
          "#{inspect err}"

      path ->
        path = Path.join(Path.dirname(file), Path.relative_to(path, file))

        {quoted, new_loaded_paths} =
          if String.contains?(path, "*") do
            {{:__block__, _, quoted}, new_loaded_paths} =
              do_read_quoted_wildcard!(path, loaded_paths)

            {quoted, new_loaded_paths}
          else
            {{:__block__, _, quoted}, new_loaded_paths} = do_read_quoted!(path, loaded_paths)
            {quoted, new_loaded_paths}
          end

        new_acc =
          quoted
          |> Enum.reject(fn
            {:use, _, [{:__aliases__, _, [:Mix, :Config]}]} -> true
            _ -> false
          end)
          |> Enum.reverse()
          |> Enum.concat(acc)

        merge_imports(block, new_acc, file, new_loaded_paths)
    end
  end

  defp merge_imports([{:config, env, [:kernel | _]} = other | rest], acc, file, loaded_paths) do
    line = Keyword.get(env, :line, "N/A")
    file_path = Path.relative_to_cwd(file)

    Logger.warn(
      "Found config setting for :kernel application in Mix config!\n" <>
        "    File: #{file_path}\n" <>
        "    Line: #{line}\n" <>
        "    Any :kernel config settings need to be placed in vm.args, or they will not take effect!"
    )

    merge_imports(rest, [other | acc], file, loaded_paths)
  end

  defp merge_imports([other | rest], acc, file, loaded_paths) do
    merge_imports(rest, [other | acc], file, loaded_paths)
  end

  defp eval_path(_acc, path) when is_binary(path) do
    path
  end

  defp eval_path(acc, expr) do
    # Rebuild script context without Mix.Config macros
    stripped = strip_config_macros(acc, [expr])
    quoted = {:__block__, [], stripped}

    try do
      {path, _bindings} = Code.eval_quoted(quoted)
      path
    rescue
      e ->
        {:error, e}
    end
  end

  defp strip_config_macros([], acc), do: acc

  defp strip_config_macros([{:use, _, [{:__aliases__, _, [:Mix, :Config]}]} | rest], acc) do
    strip_config_macros(rest, acc)
  end

  defp strip_config_macros([{type, _, _} | rest], acc) when type in [:import_config, :config] do
    strip_config_macros(rest, acc)
  end

  defp strip_config_macros([expr | rest], acc) do
    strip_config_macros(rest, [expr | acc])
  end
end


Alphabetizing SIG resource tags

Alphabetizing SIG resource tags


use 'Task.await' to parallelize post queries

defmodule Ello.Core.Content do
  import Ecto.Query
  alias Ello.Core.{
    Repo,
    Redis,
    Network,
    Discovery,
  }
  alias __MODULE__.{
    PostsPage,
    Post,
    Love,
    Watch,
    Asset,
  }

  @moduledoc """
  Responsible for retrieving and loading posts, comments, and related assets.

  Handles database queryies, preloading reposts, and fetching cached values.
  """

  @doc """
  Get a post by id or token.

  Includes postgres info and bulk fetched redis info.

  If the current_user is passed in the reposted/wathched/loved relationship will
  also be included, and the post will be filtered based on blocked users, nsfw
  and nudity content visibility, and posts by banned users.  If no user is
  present, posts by private users will not be included.
  """

  @type filter_opts :: %{current_user: User.t | nil, allow_nsfw: boolean, allow_nudity: boolean}

  def post(id_or_slug, opts) when is_list(opts), do: post(id_or_slug, Enum.into(opts, %{}))

  @spec post(id_or_slug :: String.t | integer, filters :: filter_opts) :: Post.t
  def post("~" <> slug, %{current_user: current_user} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> Repo.get_by(token: slug)
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end
  def post(id, %{current_user: current_user} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> Repo.get(id)
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end

  def posts_by_user(user_id, opts) when is_list(opts), do: posts_by_user(user_id, Enum.into(opts, %{}))

  @spec posts_by_user(user_id :: integer, filters :: any) :: PostsPage.t
  def posts_by_user(user_id, %{} = filters) do
    per_page = parse_per_page(filters[:per_page])
    before = parse_before(filters[:before])

    total_query = total_posts_by_user_query(user_id, filters)
    remaining_query = remaining_posts_by_user_query(total_query, before)

    posts_task = Task.async(__MODULE__, :page_of_posts_by_user_query, [remaining_query, per_page, filters])
    total_count_task = Task.async(__MODULE__, :count_and_pages_calc, [total_query, per_page])
    remaining_count_task = Task.async(__MODULE__, :count_and_pages_calc, [remaining_query, per_page])

    posts = Task.await(posts_task)
    last_post_date = get_last_post_created_at(posts)
    {total_count, total_pages} = Task.await(total_count_task)
    {_, remaining_pages} = Task.await(remaining_count_task)

    %PostsPage{
      posts: posts,
      total_pages: total_pages,
      total_count: total_count,
      total_pages_remaining: remaining_pages,
      per_page: per_page,
      before: last_post_date,
    }
  end

  defp parse_per_page(per_page) when is_binary(per_page) do
    case Integer.parse(per_page) do
      {val, _} -> val
      _        -> parse_per_page(nil)
    end
  end
  defp parse_per_page(per_page) when is_integer(per_page), do: per_page
  defp parse_per_page(_), do: 25

  defp parse_before(%DateTime{} = before), do: before
  defp parse_before(before) do
    case DateTime.from_iso8601(before) do
      {:ok, date, _} -> date
      _  -> nil
    end
  end

  defp total_posts_by_user_query(user_id, %{} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> where([p], p.author_id == ^user_id and is_nil(p.parent_post_id))
  end

  defp remaining_posts_by_user_query(total_query, nil), do: total_query
  defp remaining_posts_by_user_query(total_query, date) do
    where(total_query, [p], p.created_at < ^date)
  end

  def page_of_posts_by_user_query(remaining_query, per_page, %{current_user: current_user} = _filters) do
    remaining_query
    |> order_by([p], [desc: p.created_at])
    |> limit(^per_page)
    |> Repo.all
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end

  defp get_last_post_created_at([]), do: nil
  defp get_last_post_created_at(posts) do
    List.last(posts).created_at
  end

  def count_and_pages_calc(query, per_page) do
    count = Repo.aggregate(query, :count, :id)
    {count, round(Float.ceil(count / per_page))}
  end

  defp filter_post_for_client(query, %{current_user: current_user, allow_nsfw: allow_nsfw, allow_nudity: allow_nudity}) do
    query
    |> filter_nsfw(allow_nsfw)
    |> filter_nudity(allow_nudity)
    |> filter_banned
    |> filter_private(current_user)
  end

  defp filter_nsfw(query, true), do: query
  defp filter_nsfw(query, false), do: where(query, [p], not p.is_adult_content)

  defp filter_nudity(query, true), do: query
  defp filter_nudity(query, false), do: where(query, [p], not p.has_nudity)

  defp filter_blocked(nil, _), do: nil
  defp filter_blocked([], _), do: []
  defp filter_blocked(post_or_posts, nil), do: post_or_posts
  defp filter_blocked(post_or_posts, %{all_blocked_ids: %{map: blocked}})
       when map_size(blocked) == 0,
       do: post_or_posts
  defp filter_blocked(%Post{} = post, %{all_blocked_ids: blocked}),
    do: if is_blocked(post, blocked), do: nil, else: post
  defp filter_blocked(posts, %{all_blocked_ids: blocked}),
    do: Enum.reject(posts, &is_blocked(&1, blocked))

  defp is_blocked(%{reposted_source: %{author_id: rp_id}, author_id: id}, blocked),
    do: id in blocked || rp_id in blocked
  defp is_blocked(%{author_id: id}, blocked),
    do: id in blocked

  defp filter_banned(query) do
    query
    |> join(:inner, [p], a in assoc(p, :author))
    |> join(:left, [p, a], rp in assoc(p, :reposted_source))
    |> join(:left, [p, a, rp], rpa in assoc(rp, :author))
    |> where([p, a, rp, rpa], is_nil(a.locked_at) and is_nil(rpa.locked_at))
  end

  defp filter_private(query, nil) do
    query
    |> join(:inner, [p], a in assoc(p, :author))
    |> join(:left, [p, a], rp in assoc(p, :reposted_source))
    |> join(:left, [p, a, rp], rpa in assoc(rp, :author))
    |> where([p, a, rp, rpa], a.is_public and (is_nil(rpa.is_public) or rpa.is_public))
  end
  defp filter_private(query, _), do: query

  defp post_preloads(post_or_posts, current_user) do
    post_or_posts
    |> post_and_repost_preloads(current_user)
    |> prefetch_reposted_source(current_user)
  end

  defp repost_preloads(reposts, current_user) do
    reposts
    |> post_and_repost_preloads(current_user)
    |> Enum.map(&(nilify_reposted_source(&1)))
  end

  defp nilify_reposted_source(repost) do
    Map.put(repost, :reposted_source, nil)
  end

  defp post_and_repost_preloads(posts, current_user) do
    posts
    |> prefetch_categories
    |> prefetch_assets_and_author(current_user)
    |> prefetch_current_user_relationships(current_user)
    |> prefetch_post_counts
    |> build_image_structs
  end

  defp prefetch_assets_and_author(nil, _), do: nil
  defp prefetch_assets_and_author([], _), do: []
  defp prefetch_assets_and_author(post_or_posts, current_user) do
    Repo.preload(post_or_posts, [assets: [], author: &Network.users(&1, current_user)])
  end

  defp prefetch_reposted_source(nil, _), do: nil
  defp prefetch_reposted_source([], _), do: []
  defp prefetch_reposted_source(post_or_posts, current_user) do
    Repo.preload post_or_posts, reposted_source: fn(ids) ->
      Post
      |> where([p], p.id in ^ids)
      |> Repo.all
      |> repost_preloads(current_user)
    end
  end

  defp prefetch_current_user_relationships(post_or_posts, nil), do: post_or_posts
  defp prefetch_current_user_relationships(nil, _), do: nil
  defp prefetch_current_user_relationships([], _), do: []
  defp prefetch_current_user_relationships(post_or_posts, %{id: id}) do
    current_user_repost_query = where(Post, author_id: ^id)
    current_user_love_query = where(Love, user_id: ^id)
    current_user_watch_query = where(Watch, user_id: ^id)
    Repo.preload(post_or_posts, [
      repost_from_current_user: current_user_repost_query,
      love_from_current_user:   current_user_love_query,
      watch_from_current_user:  current_user_watch_query,
    ])
  end

  # Because categories are stored as an array on posts we can use preload.
  # Instead we basically do what preload does ourselves manually.
  defp prefetch_categories(post_or_posts) do
    Discovery.put_belongs_to_many_categories(post_or_posts)
  end

  defp prefetch_post_counts(nil), do: nil
  defp prefetch_post_counts([]), do: []
  defp prefetch_post_counts(%Post{} = post),
    do: hd(prefetch_post_counts([post]))
  defp prefetch_post_counts(posts) do
    # Get counts from redis
    {:ok, counts} = Redis.command(["MGET" | count_keys_for_posts(posts)], name: :post_counts)

    # Add counts to posts
    counts
    |> Enum.map(&(String.to_integer(&1 || "0")))
    |> Enum.chunk(4)
    |> Enum.zip(posts)
    |> Enum.map(fn({[loves, comments, reposts, views], user}) ->
      Map.merge user, %{
        loves_count:    loves,
        comments_count: comments,
        reposts_count:  reposts,
        views_count:    views,
      }
    end)
  end

  defp count_keys_for_posts(posts) do
    # Get keys for each counter
    Enum.flat_map posts, fn(%{id: id}) ->
      [
        "post:#{id}:love_counter",
        "post:#{id}:comment_counter",
        "post:#{id}:repost_counter",
        "post:#{id}:view_counter",
      ]
    end
  end

  defp build_image_structs(%Post{assets: assets} = post) when is_list(assets) do
    built_assets = assets
                   |> Enum.reject(&(is_nil(&1.attachment)))
                   |> Enum.map(&Asset.build_attachment/1)
    Map.put(post, :assets, built_assets)
  end
  defp build_image_structs(%Post{} = post), do: post
  defp build_image_structs(nil), do: nil
  defp build_image_structs(posts) when is_list(posts) do
    Enum.map(posts, &build_image_structs/1)
  end
end

defmodule Ello.Core.Content do
  import Ecto.Query
  alias Ello.Core.{
    Repo,
    Redis,
    Network,
    Discovery,
  }
  alias __MODULE__.{
    PostsPage,
    Post,
    Love,
    Watch,
    Asset,
  }

  @moduledoc """
  Responsible for retrieving and loading posts, comments, and related assets.

  Handles database queryies, preloading reposts, and fetching cached values.
  """

  @doc """
  Get a post by id or token.

  Includes postgres info and bulk fetched redis info.

  If the current_user is passed in the reposted/wathched/loved relationship will
  also be included, and the post will be filtered based on blocked users, nsfw
  and nudity content visibility, and posts by banned users.  If no user is
  present, posts by private users will not be included.
  """

  @type filter_opts :: %{current_user: User.t | nil, allow_nsfw: boolean, allow_nudity: boolean}

  def post(id_or_slug, opts) when is_list(opts), do: post(id_or_slug, Enum.into(opts, %{}))

  @spec post(id_or_slug :: String.t | integer, filters :: filter_opts) :: Post.t
  def post("~" <> slug, %{current_user: current_user} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> Repo.get_by(token: slug)
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end
  def post(id, %{current_user: current_user} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> Repo.get(id)
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end

  def posts_by_user(user_id, opts) when is_list(opts), do: posts_by_user(user_id, Enum.into(opts, %{}))

  @spec posts_by_user(user_id :: integer, filters :: any) :: PostsPage.t
  def posts_by_user(user_id, %{} = filters) do
    per_page = parse_per_page(filters[:per_page])
    before = parse_before(filters[:before])

    total_query = total_posts_by_user_query(user_id, filters)
    remaining_query = remaining_posts_by_user_query(total_query, before)
    posts = page_of_posts_by_user_query(remaining_query, per_page, filters)
    last_post_date = get_last_post_created_at(posts)
    {total_count, total_pages} = count_and_pages_calc(total_query, per_page)
    {_, remaining_pages} = count_and_pages_calc(remaining_query, per_page)

    %PostsPage{
      posts: posts,
      total_pages: total_pages,
      total_count: total_count,
      total_pages_remaining: remaining_pages,
      per_page: per_page,
      before: last_post_date,
    }
  end

  defp parse_per_page(per_page) when is_binary(per_page) do
    case Integer.parse(per_page) do
      {val, _} -> val
      _        -> parse_per_page(nil)
    end
  end
  defp parse_per_page(per_page) when is_integer(per_page), do: per_page
  defp parse_per_page(_), do: 25

  defp parse_before(%DateTime{} = before), do: before
  defp parse_before(before) do
    case DateTime.from_iso8601(before) do
      {:ok, date, _} -> date
      _  -> nil
    end
  end

  defp total_posts_by_user_query(user_id, %{} = filters) do
    Post
    |> filter_post_for_client(filters)
    |> where([p], p.author_id == ^user_id and is_nil(p.parent_post_id))
  end

  defp remaining_posts_by_user_query(total_query, nil), do: total_query
  defp remaining_posts_by_user_query(total_query, date) do
    where(total_query, [p], p.created_at < ^date)
  end

  defp page_of_posts_by_user_query(remaining_query, per_page, %{current_user: current_user} = _filters) do
    remaining_query
    |> order_by([p], [desc: p.created_at])
    |> limit(^per_page)
    |> Repo.all
    |> post_preloads(current_user)
    |> filter_blocked(current_user)
  end

  defp get_last_post_created_at([]), do: nil
  defp get_last_post_created_at(posts) do
    List.last(posts).created_at
  end

  defp count_and_pages_calc(query, per_page) do
    count = Repo.aggregate(query, :count, :id)
    {count, round(Float.ceil(count / per_page))}
  end

  defp filter_post_for_client(query, %{current_user: current_user, allow_nsfw: allow_nsfw, allow_nudity: allow_nudity}) do
    query
    |> filter_nsfw(allow_nsfw)
    |> filter_nudity(allow_nudity)
    |> filter_banned
    |> filter_private(current_user)
  end

  defp filter_nsfw(query, true), do: query
  defp filter_nsfw(query, false), do: where(query, [p], not p.is_adult_content)

  defp filter_nudity(query, true), do: query
  defp filter_nudity(query, false), do: where(query, [p], not p.has_nudity)

  defp filter_blocked(nil, _), do: nil
  defp filter_blocked([], _), do: []
  defp filter_blocked(post_or_posts, nil), do: post_or_posts
  defp filter_blocked(post_or_posts, %{all_blocked_ids: %{map: blocked}})
       when map_size(blocked) == 0,
       do: post_or_posts
  defp filter_blocked(%Post{} = post, %{all_blocked_ids: blocked}),
    do: if is_blocked(post, blocked), do: nil, else: post
  defp filter_blocked(posts, %{all_blocked_ids: blocked}),
    do: Enum.reject(posts, &is_blocked(&1, blocked))

  defp is_blocked(%{reposted_source: %{author_id: rp_id}, author_id: id}, blocked),
    do: id in blocked || rp_id in blocked
  defp is_blocked(%{author_id: id}, blocked),
    do: id in blocked

  defp filter_banned(query) do
    query
    |> join(:inner, [p], a in assoc(p, :author))
    |> join(:left, [p, a], rp in assoc(p, :reposted_source))
    |> join(:left, [p, a, rp], rpa in assoc(rp, :author))
    |> where([p, a, rp, rpa], is_nil(a.locked_at) and is_nil(rpa.locked_at))
  end

  defp filter_private(query, nil) do
    query
    |> join(:inner, [p], a in assoc(p, :author))
    |> join(:left, [p, a], rp in assoc(p, :reposted_source))
    |> join(:left, [p, a, rp], rpa in assoc(rp, :author))
    |> where([p, a, rp, rpa], a.is_public and (is_nil(rpa.is_public) or rpa.is_public))
  end
  defp filter_private(query, _), do: query

  defp post_preloads(post_or_posts, current_user) do
    post_or_posts
    |> post_and_repost_preloads(current_user)
    |> prefetch_reposted_source(current_user)
  end

  defp repost_preloads(reposts, current_user) do
    reposts
    |> post_and_repost_preloads(current_user)
    |> Enum.map(&(nilify_reposted_source(&1)))
  end

  defp nilify_reposted_source(repost) do
    Map.put(repost, :reposted_source, nil)
  end

  defp post_and_repost_preloads(posts, current_user) do
    posts
    |> prefetch_categories
    |> prefetch_assets_and_author(current_user)
    |> prefetch_current_user_relationships(current_user)
    |> prefetch_post_counts
    |> build_image_structs
  end

  defp prefetch_assets_and_author(nil, _), do: nil
  defp prefetch_assets_and_author([], _), do: []
  defp prefetch_assets_and_author(post_or_posts, current_user) do
    Repo.preload(post_or_posts, [assets: [], author: &Network.users(&1, current_user)])
  end

  defp prefetch_reposted_source(nil, _), do: nil
  defp prefetch_reposted_source([], _), do: []
  defp prefetch_reposted_source(post_or_posts, current_user) do
    Repo.preload post_or_posts, reposted_source: fn(ids) ->
      Post
      |> where([p], p.id in ^ids)
      |> Repo.all
      |> repost_preloads(current_user)
    end
  end

  defp prefetch_current_user_relationships(post_or_posts, nil), do: post_or_posts
  defp prefetch_current_user_relationships(nil, _), do: nil
  defp prefetch_current_user_relationships([], _), do: []
  defp prefetch_current_user_relationships(post_or_posts, %{id: id}) do
    current_user_repost_query = where(Post, author_id: ^id)
    current_user_love_query = where(Love, user_id: ^id)
    current_user_watch_query = where(Watch, user_id: ^id)
    Repo.preload(post_or_posts, [
      repost_from_current_user: current_user_repost_query,
      love_from_current_user:   current_user_love_query,
      watch_from_current_user:  current_user_watch_query,
    ])
  end

  # Because categories are stored as an array on posts we can use preload.
  # Instead we basically do what preload does ourselves manually.
  defp prefetch_categories(post_or_posts) do
    Discovery.put_belongs_to_many_categories(post_or_posts)
  end

  defp prefetch_post_counts(nil), do: nil
  defp prefetch_post_counts([]), do: []
  defp prefetch_post_counts(%Post{} = post),
    do: hd(prefetch_post_counts([post]))
  defp prefetch_post_counts(posts) do
    # Get counts from redis
    {:ok, counts} = Redis.command(["MGET" | count_keys_for_posts(posts)], name: :post_counts)

    # Add counts to posts
    counts
    |> Enum.map(&(String.to_integer(&1 || "0")))
    |> Enum.chunk(4)
    |> Enum.zip(posts)
    |> Enum.map(fn({[loves, comments, reposts, views], user}) ->
      Map.merge user, %{
        loves_count:    loves,
        comments_count: comments,
        reposts_count:  reposts,
        views_count:    views,
      }
    end)
  end

  defp count_keys_for_posts(posts) do
    # Get keys for each counter
    Enum.flat_map posts, fn(%{id: id}) ->
      [
        "post:#{id}:love_counter",
        "post:#{id}:comment_counter",
        "post:#{id}:repost_counter",
        "post:#{id}:view_counter",
      ]
    end
  end

  defp build_image_structs(%Post{assets: assets} = post) when is_list(assets) do
    built_assets = assets
                   |> Enum.reject(&(is_nil(&1.attachment)))
                   |> Enum.map(&Asset.build_attachment/1)
    Map.put(post, :assets, built_assets)
  end
  defp build_image_structs(%Post{} = post), do: post
  defp build_image_structs(nil), do: nil
  defp build_image_structs(posts) when is_list(posts) do
    Enum.map(posts, &build_image_structs/1)
  end
end


put brand_account in links under 'users'

defmodule Ello.V2.CategoryView do
  use Ello.V2.Web, :view
  use Ello.V2.JSONAPI
  alias Ello.V2.{ImageView, PromotionalView, UserView}

  def stale_checks(_, %{data: categories}) do
    [etag: etag(categories)]
  end

  def render("index.json", %{data: categories} = opts) do
    promotionals = Enum.flat_map(categories, &(&1.promotionals))
    users = Enum.map(promotionals, &(&1.user))
    brand_accounts = Enum.map(categories, &pluck_brand_account/1)

    json_response()
    |> render_resource(:categories, categories, __MODULE__, opts)
    |> include_linked(:promotionals, promotionals, PromotionalView, opts)
    |> include_linked(:users, users, UserView, opts)
    |> include_linked(:users, brand_accounts, UserView, opts)
  end

  @doc "Render categories and relations for /api/v2/categories/:id"
  def render("show.json", %{data: category} = opts) do
    users = Enum.map(category.promotionals, &(&1.user))

    json_response()
    |> render_resource(:categories, category, __MODULE__, opts)
    |> include_linked(:promotionals, category.promotionals, PromotionalView, opts)
    |> include_linked(:users, users, UserView, opts)
    |> include_linked(:users, pluck_brand_account(category), UserView, opts)
  end

  @doc "Render a single category as included in other reponses"
  def render("category.json", %{category: category} = opts) do
    render_self(category, __MODULE__, opts)
  end

  def attributes, do: [
    :name,
    :cta_caption,
    :cta_href,
    :description,
    :is_sponsored,
    :level,
    :order,
    :slug,
    :uses_page_promotionals,
    :allow_in_onboarding,
    :is_creator_type,
  ]

  def computed_attributes, do: [
    :header,
    :tile_image
  ]

  def tile_image(category, conn) do
    render(ImageView, "image.json", image: category.tile_image_struct, conn: conn)
  end

  # Only link promotionals if they are preloaded - this does not happen when
  # sideloading categories with users.
  def links(%{promotionals: promos} = category, _) when is_list(promos) do
    %{
      brand_account: brand_account(category),
      promotionals: Enum.map(promos, &("#{&1.id}")),
      recent: %{related: related_link(category)},
    }
  end
  def links(category, _) do
    %{
      brand_account: brand_account(category),
      recent: %{related: related_link(category)},
    }
  end

  defp pluck_brand_account(%{brand_account: %Ello.Core.Network.User{} = brand_account}), do: brand_account
  defp pluck_brand_account(_), do: nil

  defp brand_account(%{brand_account: %Ello.Core.Network.User{} = user}) do
    %{
      id: "#{user.id}",
      type: "users",
      href: "/api/v2/users/#{user.id}",
    }
  end
  defp brand_account(_), do: nil

  defp related_link(%{slug: slug}) do
    "/api/v2/categories/#{slug}/posts/recent"
  end

  def header(%{header: nil, name: name}, _), do: name
  def header(%{header: "", name: name}, _),  do: name
  def header(%{header: header, name: _}, _), do: header
end

defmodule Ello.V2.CategoryView do
  use Ello.V2.Web, :view
  use Ello.V2.JSONAPI
  alias Ello.V2.{ImageView, PromotionalView, UserView}

  def stale_checks(_, %{data: categories}) do
    [etag: etag(categories)]
  end

  def render("index.json", %{data: categories} = opts) do
    promotionals = Enum.flat_map(categories, &(&1.promotionals))
    users = Enum.map(promotionals, &(&1.user))
    brand_accounts = Enum.map(categories, &pluck_brand_account/1)

    json_response()
    |> render_resource(:categories, categories, __MODULE__, opts)
    |> include_linked(:promotionals, promotionals, PromotionalView, opts)
    |> include_linked(:users, users, UserView, opts)
    |> include_linked(:brand_account, brand_accounts, UserView, opts)
  end

  @doc "Render categories and relations for /api/v2/categories/:id"
  def render("show.json", %{data: category} = opts) do
    users = Enum.map(category.promotionals, &(&1.user))

    json_response()
    |> render_resource(:categories, category, __MODULE__, opts)
    |> include_linked(:promotionals, category.promotionals, PromotionalView, opts)
    |> include_linked(:users, users, UserView, opts)
    |> include_linked(:brand_account, pluck_brand_account(category), UserView, opts)
  end

  @doc "Render a single category as included in other reponses"
  def render("category.json", %{category: category} = opts) do
    render_self(category, __MODULE__, opts)
  end

  def attributes, do: [
    :name,
    :cta_caption,
    :cta_href,
    :description,
    :is_sponsored,
    :level,
    :order,
    :slug,
    :uses_page_promotionals,
    :allow_in_onboarding,
    :is_creator_type,
  ]

  def computed_attributes, do: [
    :header,
    :tile_image
  ]

  def tile_image(category, conn) do
    render(ImageView, "image.json", image: category.tile_image_struct, conn: conn)
  end

  # Only link promotionals if they are preloaded - this does not happen when
  # sideloading categories with users.
  def links(%{promotionals: promos} = category, _) when is_list(promos) do
    %{
      brand_account: brand_account(category),
      promotionals: Enum.map(promos, &("#{&1.id}")),
      recent: %{related: related_link(category)},
    }
  end
  def links(category, _) do
    %{
      brand_account: brand_account(category),
      recent: %{related: related_link(category)},
    }
  end

  defp pluck_brand_account(%{brand_account: %Ello.Core.Network.User{} = brand_account}), do: brand_account
  defp pluck_brand_account(_), do: nil

  defp brand_account(%{brand_account: %Ello.Core.Network.User{} = user}) do
    %{
      id: "#{user.id}",
      type: "users",
      href: "/api/v2/users/#{user.id}",
    }
  end
  defp brand_account(_), do: nil

  defp related_link(%{slug: slug}) do
    "/api/v2/categories/#{slug}/posts/recent"
  end

  def header(%{header: nil, name: name}, _), do: name
  def header(%{header: "", name: name}, _),  do: name
  def header(%{header: header, name: _}, _), do: header
end


Fix code style in tests for OptionParser (#5714)

Code.require_file "test_helper.exs", __DIR__

defmodule OptionParserTest do
  use ExUnit.Case, async: true

  doctest OptionParser

  test "parses boolean option" do
    assert OptionParser.parse(["--docs"]) == {[docs: true], [], []}
  end

  test "parses alias boolean option as the alias key" do
    assert OptionParser.parse(["-d"], aliases: [d: :docs]) ==
           {[docs: true], [], []}
  end

  test "parses more than one boolean option" do
    assert OptionParser.parse(["--docs", "--compile"]) ==
           {[docs: true, compile: true], [], []}
  end

  test "parses more than one boolean options as the alias" do
    assert OptionParser.parse(["-d", "--compile"], aliases: [d: :docs]) ==
           {[docs: true, compile: true], [], []}
  end

  test "parses --key value option" do
    assert OptionParser.parse(["--source", "form_docs/"]) ==
           {[source: "form_docs/"], [], []}
  end

  test "parses only to existing atoms" do
    assert OptionParser.parse(["--option-key-does-not-exist"]) ==
           {[], [], [{"--option-key-does-not-exist", nil}]}
  end

  test "parses --key=value option" do
    assert OptionParser.parse(["--source=form_docs/", "other"]) ==
           {[source: "form_docs/"], ["other"], []}
  end

  test "parses alias --key value option as the alias" do
    assert OptionParser.parse(["-s", "from_docs/"], aliases: [s: :source]) ==
           {[source: "from_docs/"], [], []}
  end

  test "parses alias --key=value option as the alias" do
    assert OptionParser.parse(["-s=from_docs/", "other"], aliases: [s: :source]) ==
           {[source: "from_docs/"], ["other"], []}
  end

  test "does not interpret undefined options with value as boolean" do
    assert OptionParser.parse(["--no-bool"]) ==
           {[no_bool: true], [], []}
    assert OptionParser.parse(["--no-bool"], strict: []) ==
           {[], [], [{"--no-bool", nil}]}
    assert OptionParser.parse(["--no-bool=...", "other"]) ==
           {[no_bool: "..."], ["other"], []}
  end

  test "does not parse -- as an alias" do
    assert OptionParser.parse(["--s=from_docs/"], aliases: [s: :source]) ==
           {[s: "from_docs/"], [], []}
  end

  test "parses -ab as -a -b" do
    aliases = [a: :first, b: :second]

    assert OptionParser.parse(["-ab"], aliases: aliases) ==
           {[first: true, second: true], [], []}

    assert OptionParser.parse(["-ab=1"], aliases: aliases, switches: [second: :integer]) ==
           {[first: true, second: 1], [], []}

    assert OptionParser.parse(["-ab", "1"], aliases: aliases, switches: [second: :integer]) ==
           {[first: true, second: 1], [], []}
  end

  test "parses configured booleans" do
    assert OptionParser.parse(["--docs=false"], switches: [docs: :boolean]) ==
           {[docs: false], [], []}
    assert OptionParser.parse(["--docs=true"], switches: [docs: :boolean]) ==
           {[docs: true], [], []}
    assert OptionParser.parse(["--docs=other"], switches: [docs: :boolean]) ==
           {[], [], [{"--docs", "other"}]}
    assert OptionParser.parse(["--docs="], switches: [docs: :boolean]) ==
           {[], [], [{"--docs", ""}]}

    assert OptionParser.parse(["--docs", "foo"], switches: [docs: :boolean]) ==
           {[docs: true], ["foo"], []}
    assert OptionParser.parse(["--no-docs", "foo"], switches: [docs: :boolean]) ==
           {[docs: false], ["foo"], []}
    assert OptionParser.parse(["--no-docs=foo", "bar"], switches: [docs: :boolean]) ==
           {[], ["bar"], [{"--no-docs", "foo"}]}
    assert OptionParser.parse(["--no-docs=", "bar"], switches: [docs: :boolean]) ==
           {[], ["bar"], [{"--no-docs", ""}]}
  end

  test "does not set unparsed booleans" do
    assert OptionParser.parse(["foo"], switches: [docs: :boolean]) ==
           {[], ["foo"], []}
  end

  test "keeps options on configured keep" do
    argv = ["--require", "foo", "--require", "bar", "baz"]
    assert OptionParser.parse(argv, switches: [require: :keep]) ==
           {[require: "foo", require: "bar"], ["baz"], []}

    assert OptionParser.parse(["--require"], switches: [require: :keep]) ==
           {[], [], [{"--require", nil}]}
  end

  test "parses configured strings" do
    assert OptionParser.parse(["--value", "1", "foo"], switches: [value: :string]) ==
           {[value: "1"], ["foo"], []}
    assert OptionParser.parse(["--value=1", "foo"], switches: [value: :string]) ==
           {[value: "1"], ["foo"], []}
    assert OptionParser.parse(["--value"], switches: [value: :string]) ==
           {[], [], [{"--value", nil}]}
    assert OptionParser.parse(["--no-value"], switches: [value: :string]) ==
           {[no_value: true], [], []}
  end

  test "parses configured counters" do
    assert OptionParser.parse(["--verbose"], switches: [verbose: :count]) ==
           {[verbose: 1], [], []}
    assert OptionParser.parse(["--verbose", "--verbose"], switches: [verbose: :count]) ==
           {[verbose: 2], [], []}
    assert OptionParser.parse(["--verbose", "-v", "-v", "--", "bar"],
                              aliases: [v: :verbose], strict: [verbose: :count]) ==
           {[verbose: 3], ["bar"], []}
  end

  test "parses configured integers" do
    assert OptionParser.parse(["--value", "1", "foo"], switches: [value: :integer]) ==
           {[value: 1], ["foo"], []}
    assert OptionParser.parse(["--value=1", "foo"], switches: [value: :integer]) ==
           {[value: 1], ["foo"], []}
    assert OptionParser.parse(["--value", "WAT", "foo"], switches: [value: :integer]) ==
           {[], ["foo"], [{"--value", "WAT"}]}
  end

  test "parses configured integers with keep" do
    argv = ["--value", "1", "--value", "2", "foo"]
    assert OptionParser.parse(argv, switches: [value: [:integer, :keep]]) ==
           {[value: 1, value: 2], ["foo"], []}

    argv = ["--value=1", "foo", "--value=2", "bar"]
    assert OptionParser.parse(argv, switches: [value: [:integer, :keep]]) ==
           {[value: 1, value: 2], ["foo", "bar"], []}
  end

  test "parses configured floats" do
    assert OptionParser.parse(["--value", "1.0", "foo"], switches: [value: :float]) ==
           {[value: 1.0], ["foo"], []}
    assert OptionParser.parse(["--value=1.0", "foo"], switches: [value: :float]) ==
           {[value: 1.0], ["foo"], []}
    assert OptionParser.parse(["--value", "WAT", "foo"], switches: [value: :float]) ==
           {[], ["foo"], [{"--value", "WAT"}]}
  end

  test "overrides options by default" do
    assert OptionParser.parse(["--require", "foo", "--require", "bar", "baz"]) ==
           {[require: "bar"], ["baz"], []}
  end

  test "parses mixed options" do
    argv = ["--source", "from_docs/", "--compile", "-x"]
    assert OptionParser.parse(argv, aliases: [x: :x]) ==
           {[source: "from_docs/", compile: true, x: true], [], []}
  end

  test "stops on first non-option arguments" do
    argv = ["--source", "from_docs/", "test/enum_test.exs", "--verbose"]
    assert OptionParser.parse_head(argv) ==
           {[source: "from_docs/"], ["test/enum_test.exs", "--verbose"], []}
  end

  test "stops on --" do
    options = OptionParser.parse(["--source", "foo", "--", "1", "2", "3"])
    assert options == {[source: "foo"], ["1", "2", "3"], []}

    options = OptionParser.parse_head(["--source", "foo", "--", "1", "2", "3"])
    assert options == {[source: "foo"], ["1", "2", "3"], []}

    options = OptionParser.parse(["--source", "foo", "bar", "--", "-x"])
    assert options == {[source: "foo"], ["bar", "-x"], []}

    options = OptionParser.parse_head(["--source", "foo", "bar", "--", "-x"])
    assert options == {[source: "foo"], ["bar", "--", "-x"], []}
  end

  test "goes beyond the first non-option arguments" do
    argv = ["--source", "from_docs/", "test/enum_test.exs", "--verbose"]
    assert OptionParser.parse(argv) ==
           {[source: "from_docs/", verbose: true], ["test/enum_test.exs"], []}
  end

  test "parses more than one key/value options" do
    assert OptionParser.parse(["--source", "from_docs/", "--docs", "show"]) ==
           {[source: "from_docs/", docs: "show"], [], []}
  end

  test "collects multiple invalid options" do
    argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
    assert OptionParser.parse(argv, switches: [bad: :integer]) ==
           {[], ["foo", "bar"], [{"--bad", "opt"}, {"-o", "bad"}]}
  end

  test "parses more than one key/value options using strict" do
    assert OptionParser.parse(["--source", "from_docs/", "--docs", "show"],
                              strict: [source: :string, docs: :string]) ==
           {[source: "from_docs/", docs: "show"], [], []}

    assert OptionParser.parse(["--source", "from_docs/", "--doc", "show"],
                              strict: [source: :string, docs: :string]) ==
           {[source: "from_docs/"], ["show"], [{"--doc", nil}]}

    assert OptionParser.parse(["--source", "from_docs/", "--doc=show"],
                              strict: [source: :string, docs: :string]) ==
           {[source: "from_docs/"], [], [{"--doc", nil}]}
  end

  test "parse!/2 raise an exception for an unknown option using strict" do
    assert_raise OptionParser.ParseError, "1 error found!\n--doc : Unknown option", fn ->
      argv = ["--source", "from_docs/", "--doc", "show"]
      OptionParser.parse!(argv, strict: [source: :string, docs: :string])
    end
  end

  test "parse!/2 raise an exception when an option is of the wrong type" do
    assert_raise OptionParser.ParseError, fn ->
      argv = ["--bad", "opt", "foo", "-o", "bad", "bar"]
      OptionParser.parse!(argv, switches: [bad: :integer])
    end
  end

  test "parse_head!/2 raise an exception when an option is of the wrong type" do
    assert_raise OptionParser.ParseError, "1 error found!\n--number : Expected type integer, got \"lib\"", fn ->
      argv = ["--number", "lib", "test/enum_test.exs"]
      OptionParser.parse_head!(argv, strict: [number: :integer])
    end
  end

  test ":switches with :strict raises" do
    assert_raise ArgumentError, ":switches and :strict cannot be given together", fn ->
      OptionParser.parse([], strict: [], switches: [])
    end
  end

  test "parses - as argument" do
    assert OptionParser.parse(["-a", "-", "-", "-b", "-"], aliases: [b: :boo]) ==
           {[boo: "-"], ["-"], [{"-a", "-"}]}

    assert OptionParser.parse(["--foo", "-", "-b", "-"], strict: [foo: :boolean, boo: :string], aliases: [b: :boo]) ==
           {[foo: true, boo: "-"], ["-"], []}
  end

  test "allow nonexistent atoms" do
    assert OptionParser.parse(["--option-key-creates-atom"], allow_nonexistent_atoms: true) ==
           {[{String.to_atom("option_key_creates_atom"), true}], [], []}
  end

  test "correctly handles negative integers" do
    assert OptionParser.parse(["arg1", "-43"]) ==
           {[], ["arg1", "-43"], []}

    assert OptionParser.parse(["arg1", "-o", "-43"], switches: [option: :integer], aliases: [o: :option]) ==
           {[option: -43], ["arg1"], []}

    assert OptionParser.parse(["arg1", "--option=-43"], switches: [option: :integer]) ==
           {[option: -43], ["arg1"], []}
  end

  test "correctly handles negative floating point numbers" do
    assert OptionParser.parse(["arg1", "-43.2"]) ==
           {[], ["arg1", "-43.2"], []}

    assert OptionParser.parse(["arg1", "-o", "-43.2"], switches: [option: :float], aliases: [o: :option]) ==
           {[option: -43.2], ["arg1"], []}

    assert OptionParser.parse(["arg1", "--option=-43.2"], switches: [option: :float]) ==
           {[option: -43.2], ["arg1"], []}
  end

  test "multi-word option" do
    config = [switches: [hello_world: :boolean]]
    assert OptionParser.next(["--hello-world"], config) ==
           {:ok, :hello_world, true, []}
    assert OptionParser.next(["--no-hello-world"], config) ==
           {:ok, :hello_world, false, []}

    assert OptionParser.next(["--hello-world"], []) ==
           {:ok, :hello_world, true, []}
    assert OptionParser.next(["--no-hello-world"], []) ==
           {:ok, :no_hello_world, true, []}
    assert OptionParser.next(["--hello_world"], []) ==
           {:invalid, "--hello_world", nil, []}
    assert OptionParser.next(["--no-hello_world"], []) ==
           {:invalid, "--no-hello_world", nil, []}

    assert OptionParser.next(["--no-hello-world"], strict: []) ==
           {:undefined, "--no-hello-world", nil, []}
    assert OptionParser.next(["--no-hello_world"], strict: []) ==
           {:undefined, "--no-hello_world", nil, []}

    config = [strict: [hello_world: :boolean]]
    assert OptionParser.next(["--hello-world"], config) ==
           {:ok, :hello_world, true, []}
    assert OptionParser.next(["--no-hello-world"], config) ==
           {:ok, :hello_world, false, []}
    assert OptionParser.next(["--hello_world"], config) ==
           {:undefined, "--hello_world", nil, []}
    assert OptionParser.next(["--no-hello_world"], config) ==
           {:undefined, "--no-hello_world", nil, []}
  end

  test "next strict: good options" do
    config = [strict: [str: :string, int: :integer, bool: :boolean]]
    assert OptionParser.next(["--str", "hello", "..."], config) ==
           {:ok, :str, "hello", ["..."]}
    assert OptionParser.next(["--int=13", "..."], config) ==
           {:ok, :int, 13, ["..."]}
    assert OptionParser.next(["--bool=false", "..."], config) ==
           {:ok, :bool, false, ["..."]}
    assert OptionParser.next(["--no-bool", "..."], config) ==
           {:ok, :bool, false, ["..."]}
    assert OptionParser.next(["--bool", "..."], config) ==
           {:ok, :bool, true, ["..."]}
    assert OptionParser.next(["..."], config) ==
           {:error, ["..."]}
  end

  test "next strict: unknown options" do
    config = [strict: [bool: :boolean]]
    assert OptionParser.next(["--str", "13", "..."], config) ==
           {:undefined, "--str", nil, ["13", "..."]}
    assert OptionParser.next(["--int=hello", "..."], config) ==
           {:undefined, "--int", "hello", ["..."]}
    assert OptionParser.next(["-no-bool=other", "..."], config) ==
           {:undefined, "-no-bool", "other", ["..."]}
  end

  test "next strict: bad type" do
    config = [strict: [str: :string, int: :integer, bool: :boolean]]
    assert OptionParser.next(["--str", "13", "..."], config) ==
           {:ok, :str, "13", ["..."]}
    assert OptionParser.next(["--int=hello", "..."], config) ==
           {:invalid, "--int", "hello", ["..."]}
    assert OptionParser.next(["--int", "hello", "..."], config) ==
           {:invalid, "--int", "hello", ["..."]}
    assert OptionParser.next(["--bool=other", "..."], config) ==
           {:invalid, "--bool", "other", ["..."]}
  end

  test "next strict: missing value" do
    config = [strict: [str: :string, int: :integer, bool: :boolean]]
    assert OptionParser.next(["--str"], config) ==
           {:invalid, "--str", nil, []}
    assert OptionParser.next(["--int"], config) ==
           {:invalid, "--int", nil, []}
    assert OptionParser.next(["--bool=", "..."], config) ==
           {:invalid, "--bool", "", ["..."]}
    assert OptionParser.next(["--no-bool=", "..."], config) ==
           {:invalid, "--no-bool", "", ["..."]}
  end

  test "split" do
    assert OptionParser.split(~S[]) ==  []
    assert OptionParser.split(~S[foo]) == ["foo"]
    assert OptionParser.split(~S[foo bar]) == ["foo", "bar"]
    assert OptionParser.split(~S[  foo  bar  ]) == ["foo", "bar"]
    assert OptionParser.split(~S[foo\ bar]) == ["foo bar"]
    assert OptionParser.split(~S[foo" bar"]) == ["foo bar"]
    assert OptionParser.split(~S[foo\" bar\"]) == ["foo\"", "bar\""]
    assert OptionParser.split(~S[foo "\ bar\""]) == ["foo", "\\ bar\""]
    assert OptionParser.split(~S[foo '\"bar"\'\ ']) == ["foo", "\\\"bar\"'\\ "]
  end

  test "to_argv" do
    assert OptionParser.to_argv([foo_bar: "baz"]) ==
           ["--foo-bar", "baz"]

    assert OptionParser.to_argv([bool: true, bool: false, discarded: nil]) ==
           ["--bool", "--no-bool"]
  end

  test ":count switch type can be translated back" do
    original = ["--counter", "--counter"]
    {opts, [], []} = OptionParser.parse(original,  [switches: [counter: :count]])
    assert original == OptionParser.to_argv(opts, [switches: [counter: :count]])
  end
end


Update DNSSD implementation

Update DNSSD implementation

defmodule Genomu.DNSSD do

  use GenServer.Behaviour

  def start_link() do
    :gen_server.start_link({:local, __MODULE__}, __MODULE__, [], [])
  end

  defrecord State, registration: nil, browser: nil, instances: nil

  def init(_) do
    :erlang.process_flag(:trap_exit, true)
    :gen_server.cast(self, :announce)
    :gen_server.cast(self, :browse)
    {:ok, State.new(instances: :ets.new(__MODULE__, []))}
  end

  alias :dnssd, as: SD

  def available_instances do
    :gen_server.call(__MODULE__, :available_instances)
  end

  def handle_call(:available_instances, _from, State[instances: instances] = state) do
    result = Enum.filter_map(:ets.match(instances, {:_, :'$1'}), fn([v]) -> not is_tuple(v) end, fn([v]) -> v end) |>
             Enum.uniq
    {:reply, result, state}
  end

  def handle_cast(:announce, State[] = state) do
    env = Application.environment(:genomu)
    {:ok, ref} =
    SD.register("#{Genomu.instance_name} @ #{Genomu.instance_url}",
                "_genomu._tcp", env[:port],
                [
                  name: Genomu.instance_name,
                  system_version: Genomu.system_version,
                  url: Genomu.instance_url,
                  cluster_name: Genomu.Cluster.name,
                ])
    {:noreply, state.registration(ref)}
  end

  def handle_cast(:browse, State[] = state) do
    {:ok, ref} = SD.browse("_genomu._tcp")
    {:noreply, state.browser(ref)}
  end

  def handle_info({:dnssd, browser, {:browse, :add, {description, service, domain}}},
                  State[browser: browser, instances: instances] = state) do
    {:ok, ref} = SD.resolve(description, service, domain)
    :ets.insert(instances, [{ref, {description, service, domain}}, {description, ref}])
    {:noreply, state}
  end

  def handle_info({:dnssd, browser, {:browse, :remove, {description, _service, _domain}}},
                  State[browser: browser, instances: instances] = state) do
    :ets.delete(instances, description)
    {:noreply, state}
  end

  def handle_info({:dnssd, ref, {:resolve, {domain, port, txt}}},
                  State[instances: instances] = state) do
    SD.stop(ref)
    [{_, {description, service, _domain}}] = :ets.lookup(instances, ref)
    :ets.delete(instances, ref)
    if txt["cluster_name"] == Genomu.Cluster.name do
      :ets.insert(instances, {description, txt})
    end
    {:noreply, state}
  end

  def handle_info({:dnssd, _, {:register, :add, _}}, state) do
    {:noreply, state}
  end
  def handle_info({:dnssd, _, {:register, :remove, _}}, state) do
    {:noreply, state}
  end

  def terminate(_, State[registration: ref]) do
    SD.stop(ref)
  end

end

defmodule Genomu.DNSSD do

  use GenServer.Behaviour

  def start_link() do
    :gen_server.start_link({:local, __MODULE__}, __MODULE__, [], [])
  end

  defrecord State, registration: nil, browser: nil, instances: nil

  def init(_) do
    :erlang.process_flag(:trap_exit, true)
    :gen_server.cast(self, :announce)
    :gen_server.cast(self, :browse)
    {:ok, State.new(instances: :ets.new(__MODULE__, []))}
  end

  alias :dnssd, as: SD

  def available_instances do
    :gen_server.call(__MODULE__, :available_instances)
  end

  def handle_call(:available_instances, _from, State[instances: instances] = state) do
    result = Enum.map(:ets.match(instances, {{:_, :_}, :'$1'}), fn([v]) -> v end) |>
             Enum.uniq
    {:reply, result, state}
  end

  def handle_cast(:announce, State[] = state) do
    env = Application.environment(:genomu)
    {:ok, ref} =
    SD.register(Genomu.instance_url,
                "_genomu._tcp", env[:port],
                [
                  name: Genomu.instance_name,
                  system_version: Genomu.system_version,
                  url: Genomu.instance_url,
                  cluster_name: Genomu.Cluster.name,
                ])
    {:noreply, state.registration(ref)}
  end

  def handle_cast(:browse, State[] = state) do
    {:ok, ref} = SD.browse("_genomu._tcp")
    {:noreply, state.browser(ref)}
  end

  def handle_info({:dnssd, browser, {:browse, :add, {description, service, domain}}},
                  State[browser: browser, instances: instances] = state) do
    {:ok, ref} = SD.resolve(description, service, domain)
    :ets.insert(instances, {ref, {description, domain}})
    {:noreply, state}
  end

  def handle_info({:dnssd, browser, {:browse, :remove, {_description, _service, domain}}},
                  State[browser: browser, instances: instances] = state) do
    :ets.match_delete(instances, {{domain, :_}, :_})
    {:noreply, state}
  end

  def handle_info({:dnssd, ref, {:resolve, {domain, port, txt}}},
                  State[instances: instances] = state) do
    SD.stop(ref)
    if txt["cluster_name"] == Genomu.Cluster.name do
      :ets.insert(instances, {{domain, port}, txt})
    end
    {:noreply, state}
  end

  def handle_info({:dnssd, _, {:register, :add, _}}, state) do
    {:noreply, state}
  end
  def handle_info({:dnssd, _, {:register, :remove, _}}, state) do
    {:noreply, state}
  end

  def terminate(_, State[registration: ref]) do
    SD.stop(ref)
  end

end


send email confirmation link on registration

defmodule Pan.UserFrontendController do
  use Pan.Web, :controller
  alias Pan.Message
  alias Pan.User
  alias Pan.Like
  alias Pan.Follow
  alias Pan.Subscription
  alias Pan.Podcast

  plug :scrub_params, "user" when action in [:create, :update]

  def action(conn, _) do
    apply(__MODULE__, action_name(conn), [conn, conn.params, conn.assigns.current_user])
  end

  def index(conn, _params, _user) do
    users = Repo.all(from u in Pan.User, order_by: :name)
    render conn, "index.html", users: users
  end

  def new(conn, _params, _user) do
    changeset = User.changeset(%User{})
    render conn, "new.html", changeset: changeset
  end

  def create(conn, %{"user" => user_params}, _user) do
    changeset = User.registration_changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, user} ->
        Phoenix.Token.sign(Pan.Endpoint, "user", user.id)
        |> Pan.Email.email_confirmation_link_html_email(user.email)
        |> Pan.Mailer.deliver_now()

        conn
        |> Pan.Auth.login(user)
        |> put_flash(:info, "Your account @#{user.name} has been created!")
        |> redirect(to: category_frontend_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, params, _user) do
    id = String.to_integer(params["id"])
    user = Repo.one(from u in Pan.User, where: u.id == ^id)
           |> Repo.preload([:users_i_like,
                            :categories_i_like,
                            :podcasts_i_subscribed])

    podcast_related_likes = Repo.all(from l in Like, where: l.enjoyer_id == ^id
                                                            and not is_nil(l.podcast_id),
                                                     order_by: [desc: :inserted_at])
                            |> Repo.preload([:podcast, [episode: :podcast], [chapter: [episode: :podcast]]])

    messages = from(m in Message, where: m.creator_id == ^id,
                                  order_by: [desc: :inserted_at],
                                  preload: [:creator])
               |> Repo.paginate(params)

    render conn, "show.html", user: user,
                              podcast_related_likes: podcast_related_likes,
                              messages: messages
  end

  def edit(conn, _params, user) do
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"user" => user_params}, user) do
    changeset = User.self_change_changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, _user} ->
         conn
         |> put_flash(:info, "Account updated successfully.")
         |> redirect(to: user_frontend_path(conn, :my_profile))
      {:error, changeset} ->

         render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def edit_password(conn, _params, user) do
    changeset = User.changeset(user)
    render(conn, "edit_password.html", user: user, changeset: changeset)
  end

  def update_password(conn, %{"user" => user_params}, user) do
    changeset = User.password_update_changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, _user} ->
         conn
         |> put_flash(:info, "Password updated successfully.")
         |> redirect(to: user_frontend_path(conn, :my_profile))
      {:error, changeset} ->

         render(conn, "edit_password.html", user: user, changeset: changeset)
    end
  end

  def my_profile(conn, _params, user) do
    render conn, "my_profile.html", user: user
  end

  def my_messages(conn, params, user) do
    user_id = Integer.to_string(user.id)

    subscribed_user_ids = User.subscribed_user_ids(user_id)
    subscribed_category_ids = User.subscribed_category_ids(user_id)
    subscribed_podcast_ids = User.subscribed_podcast_ids(user_id)

    messages = from(m in Message,
               where: (m.topic == "mailboxes" and m.subtopic == ^user_id) or
                      (m.topic == "users"     and m.subtopic in ^subscribed_user_ids) or
                      (m.topic == "podcasts"  and m.subtopic in ^subscribed_podcast_ids) or
                      (m.topic == "category"  and m.subtopic in ^subscribed_category_ids),
               order_by: [desc: :inserted_at],
               preload: [:creator])
               |> Repo.paginate(params)

    render conn, "my_messages.html", user: user,
                                     messages: messages,
                                     page_number: messages.page_number
  end

  def my_podcasts(conn, _params, user) do
    user = Repo.get(User, user.id)
           |> Repo.preload(podcasts_i_subscribed: from(p in Podcast, order_by: p.title))
           |> Repo.preload(podcasts_i_follow: from(p in Podcast, order_by: p.title))

    podcast_ids = from(l in Like, where: l.enjoyer_id == ^user.id and
                                         is_nil(l.chapter_id) and
                                         is_nil(l.episode_id) and
                                         not is_nil(l.podcast_id),
                                  select: l.podcast_id)
                  |> Repo.all()

    podcasts_i_like = from(p in Podcast, where: p.id in ^podcast_ids,
                                         order_by: :title)
                      |> Repo.all()

    podcasts_subscribed_ids = from(s in Subscription, where: s.user_id == ^user.id,
                                                      select: s.podcast_id)
                              |> Repo.all()

    other_subscriber_ids = from(s in Subscription, where: s.podcast_id in ^podcasts_subscribed_ids,
                                                   select: s.user_id)
                           |> Repo.all()
                           |> Enum.uniq
                           |> List.delete(user.id)

    recommendations = from(s in Subscription, join: p in assoc(s, :podcast),
                                              where: s.user_id in ^other_subscriber_ids and
                                                     not s.podcast_id in ^podcasts_subscribed_ids,
                                              group_by: p.id,
                                              select: [count(s.podcast_id), p.id, p.title],
                                              order_by: [desc: count(s.podcast_id)],
                                              limit: 10)
                      |> Repo.all()

    users_also_liking = from(l in Like, where: l.podcast_id in ^podcast_ids and
                                               is_nil(l.chapter_id) and
                                               is_nil(l.episode_id),
                                        select: l.enjoyer_id)
                        |> Repo.all()
                        |> Enum.uniq
                        |> List.delete(user.id)

    also_liked = from(l in Like, join: p in assoc(l, :podcast),
                                 where: l.enjoyer_id in ^users_also_liking and
                                        is_nil(l.chapter_id) and
                                        is_nil(l.episode_id) and
                                        not l.podcast_id in ^podcast_ids,
                                 group_by: p.id,
                                 select: [count(l.podcast_id), p.id, p.title],
                                 order_by: [desc: count(l.podcast_id)],
                                 limit: 10)
                 |> Repo.all()

    render(conn, "my_podcasts.html", user: user,
                                     podcasts_i_like: podcasts_i_like,
                                     recommendations: recommendations,
                                     also_liked: also_liked)
  end

  def like_all_subscribed(conn, _params, user) do
    subscribed_podcast_ids = Repo.all(from s in Subscription,
                                      where: s.user_id == ^user.id,
                                      select: s.podcast_id)
    liked_ids = Repo.all(from l in Like,
                         where: l.enjoyer_id == ^user.id and
                                not is_nil(l.podcast_id) and
                                is_nil(l.chapter_id) and
                                is_nil(l.episode_id),
                         select: l.podcast_id)

    for id <- subscribed_podcast_ids do
      unless Enum.member?(liked_ids, id) do
        e = %Event{
          topic:           "podcast",
          subtopic:        Integer.to_string(id),
          current_user_id: user.id,
          podcast_id:      id,
          type:            "success",
          event:           "like"
        }
        e = %{e | content: "« liked the podcast " <>
                           Repo.get!(Podcast, e.podcast_id).title <> " »"}

        Podcast.like(e.podcast_id, e.current_user_id)
        Message.persist_event(e)
        Event.notify_subscribers(e)
      end
    end

    conn
    |> put_flash(:info, "Liked all podcasts you had subscribed to.")
    |> redirect(to: user_frontend_path(conn, :my_podcasts))
  end

  def follow_all_subscribed(conn, _params, user) do
    subscribed_podcast_ids = Repo.all(from s in Subscription,
                                      where: s.user_id == ^user.id,
                                      select: s.podcast_id)
    followed_ids = Repo.all(from f in Follow,
                         where: f.follower_id == ^user.id and
                                not is_nil(f.podcast_id),
                         select: f.podcast_id)

    for id <- subscribed_podcast_ids do
      unless Enum.member?(followed_ids, id) do
        e = %Event{
          topic:           "podcast",
          subtopic:        Integer.to_string(id),
          current_user_id: user.id,
          podcast_id:      id,
          type:            "success",
          event:           "follow"
        }
        e = %{e | content: "« followed the podcast " <>
                           Repo.get!(Podcast, e.podcast_id).title <> " »"}

        Podcast.follow(e.podcast_id, e.current_user_id)
        Message.persist_event(e)
        Event.notify_subscribers(e)
      end
    end

    conn
    |> put_flash(:info, "Followed all podcasts you had subscribed to.")
    |> redirect(to: user_frontend_path(conn, :my_podcasts))
  end
end

defmodule Pan.UserFrontendController do
  use Pan.Web, :controller
  alias Pan.Message
  alias Pan.User
  alias Pan.Like
  alias Pan.Follow
  alias Pan.Subscription
  alias Pan.Podcast

  plug :scrub_params, "user" when action in [:create, :update]

  def action(conn, _) do
    apply(__MODULE__, action_name(conn), [conn, conn.params, conn.assigns.current_user])
  end

  def index(conn, _params, _user) do
    users = Repo.all(from u in Pan.User, order_by: :name)
    render conn, "index.html", users: users
  end

  def new(conn, _params, _user) do
    changeset = User.changeset(%User{})
    render conn, "new.html", changeset: changeset
  end

  def create(conn, %{"user" => user_params}, _user) do
    changeset = User.registration_changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> Pan.Auth.login(user)
        |> put_flash(:info, "Your account @#{user.name} has been created!")
        |> redirect(to: category_frontend_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, params, _user) do
    id = String.to_integer(params["id"])
    user = Repo.one(from u in Pan.User, where: u.id == ^id)
           |> Repo.preload([:users_i_like,
                            :categories_i_like,
                            :podcasts_i_subscribed])

    podcast_related_likes = Repo.all(from l in Like, where: l.enjoyer_id == ^id
                                                            and not is_nil(l.podcast_id),
                                                     order_by: [desc: :inserted_at])
                            |> Repo.preload([:podcast, [episode: :podcast], [chapter: [episode: :podcast]]])

    messages = from(m in Message, where: m.creator_id == ^id,
                                  order_by: [desc: :inserted_at],
                                  preload: [:creator])
               |> Repo.paginate(params)

    render conn, "show.html", user: user,
                              podcast_related_likes: podcast_related_likes,
                              messages: messages
  end

  def edit(conn, _params, user) do
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"user" => user_params}, user) do
    changeset = User.self_change_changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, _user} ->
         conn
         |> put_flash(:info, "Account updated successfully.")
         |> redirect(to: user_frontend_path(conn, :my_profile))
      {:error, changeset} ->

         render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def edit_password(conn, _params, user) do
    changeset = User.changeset(user)
    render(conn, "edit_password.html", user: user, changeset: changeset)
  end

  def update_password(conn, %{"user" => user_params}, user) do
    changeset = User.password_update_changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, _user} ->
         conn
         |> put_flash(:info, "Password updated successfully.")
         |> redirect(to: user_frontend_path(conn, :my_profile))
      {:error, changeset} ->

         render(conn, "edit_password.html", user: user, changeset: changeset)
    end
  end

  def my_profile(conn, _params, user) do
    render conn, "my_profile.html", user: user
  end

  def my_messages(conn, params, user) do
    user_id = Integer.to_string(user.id)

    subscribed_user_ids = User.subscribed_user_ids(user_id)
    subscribed_category_ids = User.subscribed_category_ids(user_id)
    subscribed_podcast_ids = User.subscribed_podcast_ids(user_id)

    messages = from(m in Message,
               where: (m.topic == "mailboxes" and m.subtopic == ^user_id) or
                      (m.topic == "users"     and m.subtopic in ^subscribed_user_ids) or
                      (m.topic == "podcasts"  and m.subtopic in ^subscribed_podcast_ids) or
                      (m.topic == "category"  and m.subtopic in ^subscribed_category_ids),
               order_by: [desc: :inserted_at],
               preload: [:creator])
               |> Repo.paginate(params)

    render conn, "my_messages.html", user: user,
                                     messages: messages,
                                     page_number: messages.page_number
  end

  def my_podcasts(conn, _params, user) do
    user = Repo.get(User, user.id)
           |> Repo.preload(podcasts_i_subscribed: from(p in Podcast, order_by: p.title))
           |> Repo.preload(podcasts_i_follow: from(p in Podcast, order_by: p.title))

    podcast_ids = from(l in Like, where: l.enjoyer_id == ^user.id and
                                         is_nil(l.chapter_id) and
                                         is_nil(l.episode_id) and
                                         not is_nil(l.podcast_id),
                                  select: l.podcast_id)
                  |> Repo.all()

    podcasts_i_like = from(p in Podcast, where: p.id in ^podcast_ids,
                                         order_by: :title)
                      |> Repo.all()

    podcasts_subscribed_ids = from(s in Subscription, where: s.user_id == ^user.id,
                                                      select: s.podcast_id)
                              |> Repo.all()

    other_subscriber_ids = from(s in Subscription, where: s.podcast_id in ^podcasts_subscribed_ids,
                                                   select: s.user_id)
                           |> Repo.all()
                           |> Enum.uniq
                           |> List.delete(user.id)

    recommendations = from(s in Subscription, join: p in assoc(s, :podcast),
                                              where: s.user_id in ^other_subscriber_ids and
                                                     not s.podcast_id in ^podcasts_subscribed_ids,
                                              group_by: p.id,
                                              select: [count(s.podcast_id), p.id, p.title],
                                              order_by: [desc: count(s.podcast_id)],
                                              limit: 10)
                      |> Repo.all()

    users_also_liking = from(l in Like, where: l.podcast_id in ^podcast_ids and
                                               is_nil(l.chapter_id) and
                                               is_nil(l.episode_id),
                                        select: l.enjoyer_id)
                        |> Repo.all()
                        |> Enum.uniq
                        |> List.delete(user.id)

    also_liked = from(l in Like, join: p in assoc(l, :podcast),
                                 where: l.enjoyer_id in ^users_also_liking and
                                        is_nil(l.chapter_id) and
                                        is_nil(l.episode_id) and
                                        not l.podcast_id in ^podcast_ids,
                                 group_by: p.id,
                                 select: [count(l.podcast_id), p.id, p.title],
                                 order_by: [desc: count(l.podcast_id)],
                                 limit: 10)
                 |> Repo.all()

    render(conn, "my_podcasts.html", user: user,
                                     podcasts_i_like: podcasts_i_like,
                                     recommendations: recommendations,
                                     also_liked: also_liked)
  end

  def like_all_subscribed(conn, _params, user) do
    subscribed_podcast_ids = Repo.all(from s in Subscription,
                                      where: s.user_id == ^user.id,
                                      select: s.podcast_id)
    liked_ids = Repo.all(from l in Like,
                         where: l.enjoyer_id == ^user.id and
                                not is_nil(l.podcast_id) and
                                is_nil(l.chapter_id) and
                                is_nil(l.episode_id),
                         select: l.podcast_id)

    for id <- subscribed_podcast_ids do
      unless Enum.member?(liked_ids, id) do
        e = %Event{
          topic:           "podcast",
          subtopic:        Integer.to_string(id),
          current_user_id: user.id,
          podcast_id:      id,
          type:            "success",
          event:           "like"
        }
        e = %{e | content: "« liked the podcast " <>
                           Repo.get!(Podcast, e.podcast_id).title <> " »"}

        Podcast.like(e.podcast_id, e.current_user_id)
        Message.persist_event(e)
        Event.notify_subscribers(e)
      end
    end

    conn
    |> put_flash(:info, "Liked all podcasts you had subscribed to.")
    |> redirect(to: user_frontend_path(conn, :my_podcasts))
  end

  def follow_all_subscribed(conn, _params, user) do
    subscribed_podcast_ids = Repo.all(from s in Subscription,
                                      where: s.user_id == ^user.id,
                                      select: s.podcast_id)
    followed_ids = Repo.all(from f in Follow,
                         where: f.follower_id == ^user.id and
                                not is_nil(f.podcast_id),
                         select: f.podcast_id)

    for id <- subscribed_podcast_ids do
      unless Enum.member?(followed_ids, id) do
        e = %Event{
          topic:           "podcast",
          subtopic:        Integer.to_string(id),
          current_user_id: user.id,
          podcast_id:      id,
          type:            "success",
          event:           "follow"
        }
        e = %{e | content: "« followed the podcast " <>
                           Repo.get!(Podcast, e.podcast_id).title <> " »"}

        Podcast.follow(e.podcast_id, e.current_user_id)
        Message.persist_event(e)
        Event.notify_subscribers(e)
      end
    end

    conn
    |> put_flash(:info, "Followed all podcasts you had subscribed to.")
    |> redirect(to: user_frontend_path(conn, :my_podcasts))
  end
end