Use --no-html and --no-brunch in example

defmodule Mix.Tasks.Phx.New do
  @moduledoc """
  Creates a new Phoenix project.

  It expects the path of the project as an argument.

      mix phx.new PATH [--module MODULE] [--app APP]

  A project at the given PATH will be created. The
  application name and module name will be retrieved
  from the path, unless `--module` or `--app` is given.

  ## Options

    * `--umbrella` - generate an umbrella application,
      with one application for your domain, and
      a second application for the web interface.

    * `--app` - the name of the OTP application

    * `--module` - the name of the base module in
      the generated skeleton

    * `--database` - specify the database adapter for Ecto. One of:

        * `postgres` (https://github.com/elixir-ecto/postgrex)
        * `mysql` (https://github.com/xerions/mariaex)
        * `mssql` (https://github.com/findmypast-oss/mssqlex)

      Please check the driver docs, between parentheses, for more information
      and requirements. Defaults to "postgres".

    * `--no-brunch` - do not generate brunch files
      for static asset building. When choosing this
      option, you will need to manually handle
      JavaScript dependencies if building HTML apps

    * `--no-ecto` - do not generate Ecto files.

    * `--no-html` - do not generate HTML views.

    * `--binary-id` - use `binary_id` as primary key type
      in Ecto schemas

  When passing the `--no-ecto` flag, Phoenix generators such as
  `phx.gen.html`, `phx.gen.json` and `phx.gen.context` may no
  longer work as expected as they generate context files that rely
  on Ecto for the database access. In those cases, you can pass the
  `--no-context` flag to generate most of the HTML and JSON files
  but skip the context, allowing you to fill in the blanks as desired.

  Similarly, if `--no-html` is given, the files generated by
  `phx.gen.html` will no longer work, as important HTML components
  will be missing.

  ## Examples

      mix phx.new hello_world

  Is equivalent to:

      mix phx.new hello_world --module HelloWorld

  Or without the HTML and JS bits (useful for APIs):

      mix phx.new ~/Workspace/hello_world --no-html --no-brunch

  As an umbrella:

      mix phx.new hello --umbrella

  Would generate the following directory structure and modules:

      hello_umbrella/   Hello.Umbrella
        apps/
          hello/        Hello
          hello_web/    HelloWeb

  You can read more about umbrella projects using the
  official [Elixir guide](http://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html#umbrella-projects)
  """
  use Mix.Task
  alias Phx.New.{Generator, Project, Single, Umbrella, Web, Ecto}

  @version Mix.Project.config[:version]
  @shortdoc "Creates a new Phoenix v#{@version} application"

  @switches [dev: :boolean, brunch: :boolean, ecto: :boolean,
             app: :string, module: :string, web_module: :string,
             database: :string, binary_id: :boolean, html: :boolean,
             umbrella: :boolean]

  def run([version]) when version in ~w(-v --version) do
    Mix.shell.info "Phoenix v#{@version}"
  end
  def run(argv) do
    elixir_version_check!()
    case parse_opts(argv) do
      {_opts, []}             -> Mix.Tasks.Help.run(["phx.new"])
      {opts, [base_path | _]} ->
        generator = if opts[:umbrella], do: Umbrella, else: Single
        generate(base_path, generator, opts)
    end
  end
  def run(argv, generator) do
    elixir_version_check!()
    case parse_opts(argv) do
      {_opts, []}             -> Mix.Tasks.Help.run(["phx.new"])
      {opts, [base_path | _]} -> generate(base_path, generator, opts)
    end
  end

  def generate(base_path, generator, opts) do
    base_path
    |> Project.new(opts)
    |> generator.prepare_project()
    |> Generator.put_binding()
    |> validate_project()
    |> generator.generate()
    |> prompt_to_install_deps(generator)
  end

  defp validate_project(%Project{opts: opts} = project) do
    check_app_name!(project.app, !!opts[:app])
    check_directory_existence!(project.project_path)
    check_module_name_validity!(project.root_mod)
    check_module_name_availability!(project.root_mod)

    project
  end

  defp prompt_to_install_deps(%Project{} = project, generator) do
    install? = Mix.shell.yes?("\nFetch and install dependencies?")

    maybe_cd(project.project_path, fn ->
      mix_pending =
        install_mix(install?)

      compile =
        case mix_pending do
          [] -> Task.async(fn -> rebar_available?() && cmd("mix deps.compile") end)
          _  -> Task.async(fn -> :ok end)
        end

      brunch_pending = install_brunch(install?, project)
      Task.await(compile, :infinity)

      if Project.brunch?(project) and !System.find_executable("npm") do
        print_brunch_info(project, generator)
      end

      pending = mix_pending ++ (brunch_pending || [])
      print_missing_commands(pending, project.project_path)

      if Project.ecto?(project) do
        print_ecto_info(project, generator)
      end

      print_mix_info(generator)
    end)
  end
  defp maybe_cd(path, func), do: path && File.cd!(path, func)

  defp parse_opts(argv) do
    case OptionParser.parse(argv, strict: @switches) do
      {opts, argv, []} ->
        {opts, argv}
      {_opts, _argv, [switch | _]} ->
        Mix.raise "Invalid option: " <> switch_to_string(switch)
    end
  end
  defp switch_to_string({name, nil}), do: name
  defp switch_to_string({name, val}), do: name <> "=" <> val

  defp install_brunch(install?, project) do
    assets_path = Path.join(project.web_path || project.project_path, "assets")
    brunch_config = Path.join(assets_path, "brunch-config.js")

    maybe_cmd "cd #{relative_app_path(assets_path)} && npm install && node node_modules/brunch/bin/brunch build",
              File.exists?(brunch_config), install? && System.find_executable("npm")
  end

  defp install_mix(install?) do
    maybe_cmd "mix deps.get", true, install? && hex_available?()
  end

  defp hex_available? do
    Code.ensure_loaded?(Hex)
  end

  defp rebar_available? do
    Mix.Rebar.rebar_cmd(:rebar) && Mix.Rebar.rebar_cmd(:rebar3)
  end

  defp print_brunch_info(_project, _gen) do
    Mix.shell.info """
    Phoenix uses an optional assets build tool called brunch.io
    that requires node.js and npm. Installation instructions for
    node.js, which includes npm, can be found at http://nodejs.org.

    The command listed next expect that you have npm available.
    If you don't want brunch.io, you can re-run this generator
    with the --no-brunch option.
    """
  end

  defp print_missing_commands([], path) do
    Mix.shell.info """

    We are all set! Go into your application by running:

        $ cd #{relative_app_path(path)}
    """
  end
  defp print_missing_commands(commands, path) do
    steps = ["$ cd #{relative_app_path(path)}" | commands]
    Mix.shell.info """

    We are almost there! The following steps are missing:

        #{Enum.join(steps, "\n    ")}
    """
  end

  defp print_ecto_info(%Project{}, Web), do: :ok
  defp print_ecto_info(%Project{app_path: nil}, _gen), do: :ok
  defp print_ecto_info(%Project{app_path: app_path} = project, _gen) do
    config_path =
      app_path
      |> Path.join("config/dev.exs")
      |> Path.relative_to(project.project_path)

    Mix.shell.info """
    Then configure your database in #{config_path} and run:

        $ mix ecto.create
    """
  end

  defp print_mix_info(gen) when gen in [Ecto] do
    Mix.shell.info """
    You can run your app inside IEx (Interactive Elixir) as:

        $ iex -S mix
    """
  end
  defp print_mix_info(_gen) do
    Mix.shell.info """
    Start your Phoenix app with:

        $ mix phx.server

    You can also run your app inside IEx (Interactive Elixir) as:

        $ iex -S mix phx.server
    """
  end
  defp relative_app_path(path) do
    case Path.relative_to_cwd(path) do
      ^path -> Path.basename(path)
      rel   -> rel
    end
  end

  ## Helpers

  @doc false
  def recompile(regex) do
    if Code.ensure_loaded?(Regex) and function_exported?(Regex, :recompile!, 1) do
      apply(Regex, :recompile!, [regex])
    else
      regex
    end
  end

  defp maybe_cmd(cmd, should_run?, can_run?) do
    cond do
      should_run? && can_run? ->
        cmd(cmd)
      should_run? ->
        ["$ #{cmd}"]
      true ->
        []
    end
  end

  defp cmd(cmd) do
    Mix.shell.info [:green, "* running ", :reset, cmd]
    case Mix.shell.cmd(cmd, quiet: true) do
      0 ->
        []
      _ ->
        ["$ #{cmd}"]
    end
  end

  defp check_app_name!(name, from_app_flag) do
    unless name =~ recompile(~r/^[a-z][\w_]*$/) do
      extra =
        if !from_app_flag do
          ". The application name is inferred from the path, if you'd like to " <>
          "explicitly name the application then use the `--app APP` option."
        else
          ""
        end

      Mix.raise "Application name must start with a letter and have only lowercase " <>
                "letters, numbers and underscore, got: #{inspect name}" <> extra
    end
  end

  defp check_module_name_validity!(name) do
    unless inspect(name) =~ recompile(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
      Mix.raise "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect name}"
    end
  end

  defp check_module_name_availability!(name) do
    [name]
    |> Module.concat()
    |> Module.split()
    |> Enum.reduce([], fn name, acc ->
        mod = Module.concat([Elixir, name | acc])
        if Code.ensure_loaded?(mod) do
          Mix.raise "Module name #{inspect mod} is already taken, please choose another name"
        else
          [name | acc]
        end
    end)
  end

  defp check_directory_existence!(path) do
    if File.dir?(path) and not Mix.shell.yes?("The directory #{path} already exists. Are you sure you want to continue?") do
      Mix.raise "Please select another directory for installation."
    end
  end

  defp elixir_version_check! do
    unless Version.match?(System.version, "~> 1.5") do
      Mix.raise "Phoenix v#{@version} requires at least Elixir v1.5.\n " <>
                "You have #{System.version()}. Please update accordingly"
    end
  end
end

defmodule Mix.Tasks.Phx.New do
  @moduledoc """
  Creates a new Phoenix project.

  It expects the path of the project as an argument.

      mix phx.new PATH [--module MODULE] [--app APP]

  A project at the given PATH will be created. The
  application name and module name will be retrieved
  from the path, unless `--module` or `--app` is given.

  ## Options

    * `--umbrella` - generate an umbrella application,
      with one application for your domain, and
      a second application for the web interface.

    * `--app` - the name of the OTP application

    * `--module` - the name of the base module in
      the generated skeleton

    * `--database` - specify the database adapter for Ecto. One of:

        * `postgres` (https://github.com/elixir-ecto/postgrex)
        * `mysql` (https://github.com/xerions/mariaex)
        * `mssql` (https://github.com/findmypast-oss/mssqlex)

      Please check the driver docs, between parentheses, for more information
      and requirements. Defaults to "postgres".

    * `--no-brunch` - do not generate brunch files
      for static asset building. When choosing this
      option, you will need to manually handle
      JavaScript dependencies if building HTML apps

    * `--no-ecto` - do not generate Ecto files.

    * `--no-html` - do not generate HTML views.

    * `--binary-id` - use `binary_id` as primary key type
      in Ecto schemas

  When passing the `--no-ecto` flag, Phoenix generators such as
  `phx.gen.html`, `phx.gen.json` and `phx.gen.context` may no
  longer work as expected as they generate context files that rely
  on Ecto for the database access. In those cases, you can pass the
  `--no-context` flag to generate most of the HTML and JSON files
  but skip the context, allowing you to fill in the blanks as desired.

  Similarly, if `--no-html` is given, the files generated by
  `phx.gen.html` will no longer work, as important HTML components
  will be missing.

  ## Examples

      mix phx.new hello_world

  Is equivalent to:

      mix phx.new hello_world --module HelloWorld

  Or without the html bits (useful for APIs):

      mix phx.new ~/Workspace/hello_world --no-html

  As an umbrella:

      mix phx.new hello --umbrella

  Would generate the following directory structure and modules:

      hello_umbrella/   Hello.Umbrella
        apps/
          hello/        Hello
          hello_web/    HelloWeb

  You can read more about umbrella projects using the
  official [Elixir guide](http://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-apps.html#umbrella-projects)
  """
  use Mix.Task
  alias Phx.New.{Generator, Project, Single, Umbrella, Web, Ecto}

  @version Mix.Project.config[:version]
  @shortdoc "Creates a new Phoenix v#{@version} application"

  @switches [dev: :boolean, brunch: :boolean, ecto: :boolean,
             app: :string, module: :string, web_module: :string,
             database: :string, binary_id: :boolean, html: :boolean,
             umbrella: :boolean]

  def run([version]) when version in ~w(-v --version) do
    Mix.shell.info "Phoenix v#{@version}"
  end
  def run(argv) do
    elixir_version_check!()
    case parse_opts(argv) do
      {_opts, []}             -> Mix.Tasks.Help.run(["phx.new"])
      {opts, [base_path | _]} ->
        generator = if opts[:umbrella], do: Umbrella, else: Single
        generate(base_path, generator, opts)
    end
  end
  def run(argv, generator) do
    elixir_version_check!()
    case parse_opts(argv) do
      {_opts, []}             -> Mix.Tasks.Help.run(["phx.new"])
      {opts, [base_path | _]} -> generate(base_path, generator, opts)
    end
  end

  def generate(base_path, generator, opts) do
    base_path
    |> Project.new(opts)
    |> generator.prepare_project()
    |> Generator.put_binding()
    |> validate_project()
    |> generator.generate()
    |> prompt_to_install_deps(generator)
  end

  defp validate_project(%Project{opts: opts} = project) do
    check_app_name!(project.app, !!opts[:app])
    check_directory_existence!(project.project_path)
    check_module_name_validity!(project.root_mod)
    check_module_name_availability!(project.root_mod)

    project
  end

  defp prompt_to_install_deps(%Project{} = project, generator) do
    install? = Mix.shell.yes?("\nFetch and install dependencies?")

    maybe_cd(project.project_path, fn ->
      mix_pending =
        install_mix(install?)

      compile =
        case mix_pending do
          [] -> Task.async(fn -> rebar_available?() && cmd("mix deps.compile") end)
          _  -> Task.async(fn -> :ok end)
        end

      brunch_pending = install_brunch(install?, project)
      Task.await(compile, :infinity)

      if Project.brunch?(project) and !System.find_executable("npm") do
        print_brunch_info(project, generator)
      end

      pending = mix_pending ++ (brunch_pending || [])
      print_missing_commands(pending, project.project_path)

      if Project.ecto?(project) do
        print_ecto_info(project, generator)
      end

      print_mix_info(generator)
    end)
  end
  defp maybe_cd(path, func), do: path && File.cd!(path, func)

  defp parse_opts(argv) do
    case OptionParser.parse(argv, strict: @switches) do
      {opts, argv, []} ->
        {opts, argv}
      {_opts, _argv, [switch | _]} ->
        Mix.raise "Invalid option: " <> switch_to_string(switch)
    end
  end
  defp switch_to_string({name, nil}), do: name
  defp switch_to_string({name, val}), do: name <> "=" <> val

  defp install_brunch(install?, project) do
    assets_path = Path.join(project.web_path || project.project_path, "assets")
    brunch_config = Path.join(assets_path, "brunch-config.js")

    maybe_cmd "cd #{relative_app_path(assets_path)} && npm install && node node_modules/brunch/bin/brunch build",
              File.exists?(brunch_config), install? && System.find_executable("npm")
  end

  defp install_mix(install?) do
    maybe_cmd "mix deps.get", true, install? && hex_available?()
  end

  defp hex_available? do
    Code.ensure_loaded?(Hex)
  end

  defp rebar_available? do
    Mix.Rebar.rebar_cmd(:rebar) && Mix.Rebar.rebar_cmd(:rebar3)
  end

  defp print_brunch_info(_project, _gen) do
    Mix.shell.info """
    Phoenix uses an optional assets build tool called brunch.io
    that requires node.js and npm. Installation instructions for
    node.js, which includes npm, can be found at http://nodejs.org.

    The command listed next expect that you have npm available.
    If you don't want brunch.io, you can re-run this generator
    with the --no-brunch option.
    """
  end

  defp print_missing_commands([], path) do
    Mix.shell.info """

    We are all set! Go into your application by running:

        $ cd #{relative_app_path(path)}
    """
  end
  defp print_missing_commands(commands, path) do
    steps = ["$ cd #{relative_app_path(path)}" | commands]
    Mix.shell.info """

    We are almost there! The following steps are missing:

        #{Enum.join(steps, "\n    ")}
    """
  end

  defp print_ecto_info(%Project{}, Web), do: :ok
  defp print_ecto_info(%Project{app_path: nil}, _gen), do: :ok
  defp print_ecto_info(%Project{app_path: app_path} = project, _gen) do
    config_path =
      app_path
      |> Path.join("config/dev.exs")
      |> Path.relative_to(project.project_path)

    Mix.shell.info """
    Then configure your database in #{config_path} and run:

        $ mix ecto.create
    """
  end

  defp print_mix_info(gen) when gen in [Ecto] do
    Mix.shell.info """
    You can run your app inside IEx (Interactive Elixir) as:

        $ iex -S mix
    """
  end
  defp print_mix_info(_gen) do
    Mix.shell.info """
    Start your Phoenix app with:

        $ mix phx.server

    You can also run your app inside IEx (Interactive Elixir) as:

        $ iex -S mix phx.server
    """
  end
  defp relative_app_path(path) do
    case Path.relative_to_cwd(path) do
      ^path -> Path.basename(path)
      rel   -> rel
    end
  end

  ## Helpers

  @doc false
  def recompile(regex) do
    if Code.ensure_loaded?(Regex) and function_exported?(Regex, :recompile!, 1) do
      apply(Regex, :recompile!, [regex])
    else
      regex
    end
  end

  defp maybe_cmd(cmd, should_run?, can_run?) do
    cond do
      should_run? && can_run? ->
        cmd(cmd)
      should_run? ->
        ["$ #{cmd}"]
      true ->
        []
    end
  end

  defp cmd(cmd) do
    Mix.shell.info [:green, "* running ", :reset, cmd]
    case Mix.shell.cmd(cmd, quiet: true) do
      0 ->
        []
      _ ->
        ["$ #{cmd}"]
    end
  end

  defp check_app_name!(name, from_app_flag) do
    unless name =~ recompile(~r/^[a-z][\w_]*$/) do
      extra =
        if !from_app_flag do
          ". The application name is inferred from the path, if you'd like to " <>
          "explicitly name the application then use the `--app APP` option."
        else
          ""
        end

      Mix.raise "Application name must start with a letter and have only lowercase " <>
                "letters, numbers and underscore, got: #{inspect name}" <> extra
    end
  end

  defp check_module_name_validity!(name) do
    unless inspect(name) =~ recompile(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do
      Mix.raise "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect name}"
    end
  end

  defp check_module_name_availability!(name) do
    [name]
    |> Module.concat()
    |> Module.split()
    |> Enum.reduce([], fn name, acc ->
        mod = Module.concat([Elixir, name | acc])
        if Code.ensure_loaded?(mod) do
          Mix.raise "Module name #{inspect mod} is already taken, please choose another name"
        else
          [name | acc]
        end
    end)
  end

  defp check_directory_existence!(path) do
    if File.dir?(path) and not Mix.shell.yes?("The directory #{path} already exists. Are you sure you want to continue?") do
      Mix.raise "Please select another directory for installation."
    end
  end

  defp elixir_version_check! do
    unless Version.match?(System.version, "~> 1.5") do
      Mix.raise "Phoenix v#{@version} requires at least Elixir v1.5.\n " <>
                "You have #{System.version()}. Please update accordingly"
    end
  end
end


Add nil to typespec, closes #217

Add nil to typespec, closes #217


gracefully handle failures from poller task

defmodule Discovery.Poller do
  @moduledoc """
  Notifies subscribers of node availability within Consul for a particular service.
  """

  use GenServer
  import Consul.Response, only: [consul_index: 1]

  @retry_ms 5000

  def start_link(service) when is_binary(service) do
    GenServer.start_link(__MODULE__, [service])
  end

  @spec async_poll(pid, binary | integer, binary) :: Task.t
  def async_poll(poller, index, service) do
    Task.async(__MODULE__, :poll, [poller, index, service])
  end

  @spec poll(pid, binary | integer, binary) :: :ok
  def poll(poller, index, service) do
    Consul.Catalog.service(index, service)
  end

  @spec subscribe(pid, atom, pid) :: :ok
  def subscribe(poller, module, subscriber) when is_pid(poller) and is_atom(module) and is_pid(subscriber) do
    GenServer.call(poller, {:subscribe, module, subscriber})
  end

  @spec unsubscribe(pid, atom, pid) :: :ok
  def unsubscribe(poller, module, subscriber) when is_pid(poller) and is_atom(module) and is_pid(subscriber) do
    GenServer.call(poller, {:unsubscribe, module, subscriber})
  end

  #
  # Private
  #

  defp notify_change(nodes, %{nodes: nodes}), do: :ok
  defp notify_change(nodes, %{em: em}) do
    GenEvent.notify(em, {:nodes, nodes})
  end

  #
  # GenServer callbacks
  #

  def init([service]) do
    {:ok, em} = GenEvent.start_link
    {:ok, %{em: em, service: service, nodes: [], index: nil, task: nil}, 0}
  end

  # Seed node-service data and begin polling
  def handle_info(:timeout, %{service: service, index: nil} = state) do
    case Consul.Catalog.service(service) do
      {:ok, %{body: body} = response} ->
        case Discovery.Node.build(body) do
          [] = result ->
            new_nodes = result
          nodes ->
            new_nodes = nodes
        end
        :ok       = notify_change(new_nodes, state)
        new_state = %{state | nodes: new_nodes, index: consul_index(response)}
        task      = async_poll(self, new_state.index, service)
        {:noreply, %{new_state | task: task}}
      {:error, _} ->
        {:noreply, state, @retry_ms}
    end
  end

  # Handle results from polling task
  def handle_info({ref, results}, %{task: %Task{ref: ref}} = state) do
    case results do
      {:ok, %{body: body} = response} ->
        case Discovery.Node.build(body) do
          [] = result ->
            new_nodes = result
          nodes ->
            new_nodes = nodes
        end
        :ok       = notify_change(new_nodes, state)
        new_state = %{state | nodes: new_nodes, index: consul_index(response)}
        {:noreply, new_state}
      {:error, _} ->
        new_state = %{state | index: nil}
        {:noreply, new_state, @retry_ms}
    end
  end

  # Poller completed
  def handle_info({:DOWN, ref, _, _, :normal}, %{service: service, index: index, task: %Task{ref: ref}} = state) do
    {:noreply, %{state | task: async_poll(self, index, service)}}
  end

  # Poller crashed
  def handle_info({:DOWN, ref, _, _, _}, %{task: %Task{ref: ref}} = state) do
    {:noreply, %{state | task: nil, index: nil}, @retry_ms}
  end

  def handle_call({:subscribe, module, subscriber}, _from, %{em: em, nodes: nodes} = state) do
    :ok = GenEvent.add_handler(em, {module, subscriber}, [], link: true)
    {:reply, GenEvent.notify(em, {:nodes, nodes}), state}
  end

  def handle_call({:unsubscribe, module, subscriber}, _from, %{em: em} = state) do
    {:reply, GenEvent.remove_handler(em, {module, subscriber}, []), state}
  end
end

defmodule Discovery.Poller do
  @moduledoc """
  Notifies subscribers of node availability within Consul for a particular service.
  """

  use GenServer
  import Consul.Response, only: [consul_index: 1]

  def start_link(service) when is_binary(service) do
    GenServer.start_link(__MODULE__, [service])
  end

  @spec async_poll(pid, binary | integer, binary) :: Task.t
  def async_poll(poller, index, service) do
    Task.async(__MODULE__, :poll, [poller, index, service])
  end

  @spec poll(pid, binary | integer, binary) :: :ok
  def poll(poller, index, service) do
    results = Consul.Catalog.service(index, service)
    GenServer.call(poller, {:poll_response, results})
  end

  @spec subscribe(pid, atom, pid) :: :ok
  def subscribe(poller, module, subscriber) when is_pid(poller) and is_atom(module) and is_pid(subscriber) do
    GenServer.call(poller, {:subscribe, module, subscriber})
  end

  @spec unsubscribe(pid, atom, pid) :: :ok
  def unsubscribe(poller, module, subscriber) when is_pid(poller) and is_atom(module) and is_pid(subscriber) do
    GenServer.call(poller, {:unsubscribe, module, subscriber})
  end

  #
  # Private
  #

  defp notify_change(nodes, %{nodes: nodes}), do: :ok
  defp notify_change(nodes, %{em: em}) do
    GenEvent.notify(em, {:nodes, nodes})
  end

  #
  # GenServer callbacks
  #

  def init([service]) do
    {:ok, em} = GenEvent.start_link
    {:ok, %{em: em, service: service, nodes: [], index: nil, task: nil}, 0}
  end

  def handle_info(:timeout, %{service: service, index: nil} = state) do
    case Consul.Catalog.service(service) do
      {:ok, %{body: body} = response} ->
        case Discovery.Node.build(body) do
          [] = result ->
            new_nodes = result
          nodes ->
            new_nodes = nodes
        end
        :ok       = notify_change(new_nodes, state)
        new_state = %{state | nodes: new_nodes, index: consul_index(response)}
      {:error, response} ->
        new_state = %{state | nodes: [], index: consul_index(response)}
    end

    task = async_poll(self, new_state.index, service)

    {:noreply, %{new_state | task: task}}
  end

  def handle_info({:DOWN, ref, _, _, _}, %{service: service, index: index, task: %Task{ref: ref}} = state) do
    task = async_poll(self, index, service)
    {:noreply, %{state | task: task}}
  end

  def handle_call({:poll_response, results}, _from, %{service: service} = state) do
    case results do
      {:ok, %{body: body} = response} ->
        case Discovery.Node.build(body) do
          [] = result ->
            new_nodes = result
          nodes ->
            new_nodes = nodes
        end
        :ok       = notify_change(new_nodes, state)
        new_state = %{state | nodes: new_nodes, index: consul_index(response)}
      {:error, response} ->
        new_state = %{state | nodes: [], index: consul_index(response)}
    end

    task = async_poll(self, new_state.index, service)

    {:reply, :ok, %{new_state | task: task}}
  end

  def handle_call({:subscribe, module, subscriber}, _from, %{em: em, nodes: nodes} = state) do
    :ok = GenEvent.add_handler(em, {module, subscriber}, [], link: true)
    {:reply, GenEvent.notify(em, {:nodes, nodes}), state}
  end

  def handle_call({:unsubscribe, module, subscriber}, _from, %{em: em} = state) do
    {:reply, GenEvent.remove_handler(em, {module, subscriber}, []), state}
  end
end


Updated documentation for Macro.split_call/1