docs for partials

docs for partials

defmodule Drab.Live do
  @moduledoc """
  Drab Module to provide a live access and update of assigns of the template, which is currently rendered and displayed
  in the browser.

  The idea is to reuse your Phoenix templates and let them live, to make a possibility to update assigns
  on the living page, from the Elixir, without reloading the whole stuff.

  Use `peek/2` to get the assign value, and `poke/2` to modify it directly in the displayed DOM tree.

  Drab.Live uses the modified EEx Engine (`Drab.Live.EExEngine`) to compile the template and indicate where assigns
  were rendered. To enable it, rename the template you want to go live from extension `.eex` to `.drab`. Then,
  add Drab Engine to the template engines in `config.exs`:

      config :phoenix, :template_engines,
        drab: Drab.Live.Engine

  ### Update Behaviours
  There are different behaviours of `Drab.Live`, depends on where the expression with the updated assign lives.
  For example, if the expression defines tag attribute, like `<span class="<%= @class %>">`, we don't want to
  re-render the whole tag, as it might override changes you made with other Drab module, or even with Javascript.
  Because of this, Drab finds the tag and updates only the required attributes.

  #### Plain Text
  If the expression in the template is given in any tag body, Drab will replace only the required part of the html.
  Consider the template, with initial value of `1` (given in render function in the Controller, as usual):

Chapter <%= @chapter_no %>.

  It renders to:

Chapter 1.

  This `<span>` over the expression is injected automatically by `Drab.Live.EExEngine`.
  Updating the `@chapter_no` attribute in the Drab Commander, using `poke/2`:

      chapter = peek(socket, :chapter_no)     # get the current value of `@chapter_no`
      poke(socket, chapter_no: chapter + 1)   # push the new value to the browser

  The above will change the `innerHTML` of the `<span drab-ampere=someid>` to "2":

Chapter 2.

  #### Attributes
  When the expression is defining the attribute of the tag, the behaviour if different. Let's assume there is
  a template with following html, rendered in the Controller with value of `@button` set to string `"btn-danger"`.

      <button class="btn <%= @button %>">

  It renders to:

      <button drab-ampere=someid class="btn btn-danger">

  Again, you can see injected `drab-ampere` attribute. This allows Drab to indicate where to update the attribute.
  Pushing the changes to the browser with:

      poke socket, button: "btn btn-info"

  will result with updated `class` attribute on the given tag. It is acomplished by rendering and running
  `node.setAttribute("class", "btn btn-info")`.

  Notice that the pattern where your expression lives is preserved: you may update only the partials of the
  attribute value string.

  #### Properties
  Nowadays we deal more with Node proporties than attributes. This is why `Drab.Live` introduces the special syntax.
  When using the @ sign at the beginning of the attribute name, it will be treated as a property.

      <button @hidden=<%= @hidden %>>

  Updating `@hidden` in the Drab Commander with `poke/2` will change the value of the `hidden` property
  (without dollar sign!), by sending the update javascript, like `node['hidden'] = false`.

  You may also dig deeper into the Node properties, using dot - like in JavaScript - to bind the expression
  with the specific property. The good example is to set up `.style`:

      <button @style.backgroundColor="<%= @color %>">

  Aditially, Drab sets up all the properties defined that way when the page loads. Thanks to this, you
  don't have to worry about the initial value.

  Notice that `@property=<%= expression %>` *is the only available syntax*, you can not use string pattern or
  give more than one expression. Property must be solid bind to the expression.

  The expression binded with the property *must be encodable to JS*, so, for example, tuples are not allowed here.
  Please refer to `Poison` for more information about encoding JS.

  #### Scripts
  When the assign we want to change is inside the `<script></script>` tag, Drab will re-evaluate the whole
  script after assigment change. Let's say you don't want to use `$property=<%=expression%>` syntax to define
  the object property. You may want to render the javascript:

  If you render the template in the Controller with `@button_state` set to `false`, the initial html will look like:

  Again, Drab injects some ID to know where to find its victim. After you `poke/2` the new value of `@button_state`,
  Drab will re-render the whole script with a new value and will send a request to re-evaluate the script.
  Browser will run something like: `eval("document.querySelectorAll(\"button\").hidden = true")`.

  ### Partials
  Function `poke/2` and `peek/2` works on the default template - the one rendered with the Controller. In case there
  are some child templates, rendered inside the main one, you need to specify the template name as a second argument
  of `poke/3` and `peek/3` functions.

  Assigns are archored within their partials. Manipulation of the assign outside the template it lives will raise
  `ArgumentError` in case of `poke`, or return `nil` when `peeking` the value.

  ### Limitions
  Because Drab must interpret the template, inject it's ID etc, it assumes that the template HTML is valid.
  There are also some limits for defining attributes, properties, etc. See `Drab.Live.EExEngine` for a full
  description.
  """
  import Drab.Core
  require IEx

  use DrabModule
  @doc false
  def js_templates(),  do: ["drab.events.js", "drab.live.js"]

  @doc false
  def transform_payload(payload, _state) do
    payload |> Map.put_new("value", payload["val"])
  end

  @doc false
  def transform_socket(socket, payload, state) do
    # store assigns in Drab Server
    priv = Map.merge(state.priv, %{
      __ampere_assigns: payload["__assigns"],
      __amperes: payload["__amperes"],
      __index:   payload["__index"]
    })
    Drab.pid(socket) |> Drab.set_priv(priv)
    socket
  end

  @doc """
  Returns the current value of the assign from the main partial or `nil` if not found.

      iex> peek(socket, :count)
      42
      iex> peek(socket, :nonexistent)
      nil
  """
  #TODO: think if it is needed to sign/encrypt
  def peek(socket, assign), do: peek(socket, nil, assign)

  @doc """
  Like `peek/2`, but takes partial name and returns assign from that specified partial.

  The value is take from the current

      iex> peek(socket, "users.html", :count)
      42
      iex> peek(socket, :count)
      nil

  """
  def peek(socket, partial, assign) when is_binary(assign) do
    hash = if partial, do: partial_hash(socket, partial), else: index(socket)
    assigns(socket, hash)[assign]
  end
  def peek(socket, partial, assign) when is_atom(assign), do: peek(socket, partial, Atom.to_string(assign))

  @doc """
  Updates the current page in the browser with the new assigns value.

  Returns untouched socket.

      iex> poke(socket, count: 42)
      %Phoenix.Socket{ ...
  """
  def poke(socket, assigns) do
    do_poke(socket, index(socket), assigns, &Drab.Core.exec_js/2)
  end

  @doc """
  Like `poke/2`, but limited only to the given partial name.

  Returns untouched socket.

      iex> poke(socket, "user.html", name: "Bożywój")
      %Phoenix.Socket{ ...
  """
  def poke(socket, partial, assigns) do
    hash = partial_hash(socket, partial)
    do_poke(socket, hash, assigns, &Drab.Core.exec_js/2)
  end

  @doc """
  The same as `poke/2`, but broadcasts the changes instead of pushing it to the current browser.

  See `Drab.Commander.broadcasting/1` for broadcasting options.

  Returns untouched socket.
  """
  def poke!(socket, assigns) do
    do_poke(socket, index(socket), assigns, &Drab.Core.broadcast_js/2)
  end

  @doc """
  Like `poke!/2`, but limited only to the given partial name.

  Raises `ArgumentError` when assign is not found within the partial.

  Returns untouched socket.

      iex> poke!(socket, "user.html", name: "Bożywój")
      %Phoenix.Socket{ ...
  """
  def poke!(socket, partial, assigns) do
    hash = partial_hash(socket, partial)
    do_poke(socket, hash, assigns, &Drab.Core.broadcast_js/2)
  end

  defp do_poke(socket, partial, assigns, function) do
    #TODO: improve perfomance. Now it takes 10 ms
    t1 = :os.system_time(:microsecond)
    # IO.inspect :os.system_time(:microsecond) - t1

    # partial = partial || index(socket)

    current_assigns = assigns(socket, partial)
    current_assigns_keys = Map.keys(current_assigns) |> Enum.map(&String.to_existing_atom/1)
    assigns_to_update = Enum.into(assigns, %{})
    assigns_to_update_keys = Map.keys(assigns_to_update)

    for as <- assigns_to_update_keys do
      unless Enum.find(current_assigns_keys, fn key -> key === as end) do
        raise ArgumentError, message: """
          Assign @#{as} not found in Drab EEx Template

          Please make sure all proper assigns have been set. If this
          is a child template, ensure assigns are given explicitly by
          the parent template as they are not automatically forwarded.

          Available assigns:
          #{inspect current_assigns_keys}
          """
      end
    end

    updated_assigns = current_assigns
      |> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end)
      |> Keyword.merge(assigns)

    app_module = Drab.Config.app_module()
    modules = {
      socket.assigns.__controller.__drab__().view,
      Module.concat(app_module, Router.Helpers),
      Module.concat(app_module, ErrorHelpers),
      Module.concat(app_module, Gettext)
    }

    # TODO: check only amperes which contains the changed assigns
    amperes = amperes(socket, partial)

    # construct the javascripts for update of amperes
    #TODO: group updates on one node
    update_javascripts = for ampere_hash <- amperes do
      selector = "[drab-ampere='#{ampere_hash}']"

      case Drab.Live.Cache.get(ampere_hash) do
        {:expr, expr, assigns_in_expr} ->
          # change only if poked assign exist in this ampere
          #TODO: stay DRY
          if has_common?(assigns_in_expr, assigns_to_update_keys) do
            safe = eval_expr(expr, modules, updated_assigns)
            new_value = safe_to_encoded_js(safe)

            "Drab.update_drab_span(#{encode_js(selector)}, #{new_value}, #{encode_js(partial)})"
          else
            nil
          end
        {:attribute, list} ->
          for {type, attr_or_prop, pattern, exprs, assigns_in_ampere} <- list do
            if Regex.match?(~r/{{{{@drab-ampere:[^@}]+@drab-expr-hash:[^@}]+}}}}/, attr_or_prop) do
              #TODO: special form, without atribute name
              # ignored for now, let's think if it needs to be covered
              # warning appears during compile-time
              nil
            else
              if has_common?(assigns_in_ampere, assigns_to_update_keys) do
                evaluated_expressions = Enum.map(exprs, fn hash ->
                  {:expr, expr, _} = Drab.Live.Cache.get(hash)
                  new_value = eval_expr(expr, modules, updated_assigns)
                  # new_value = safe_to_string(safe)
                  # new_value = safe
                  {hash, new_value}
                end)
                new_value_of_attribute = case type do
                  # update in pattern
                  :attr ->
                    replace_pattern(pattern, evaluated_expressions) |> encode_js()
                  :prop ->
                    {_, new_value} = evaluated_expressions |> List.first()
                    new_value |> encode_js()
                end
                sel = encode_js(selector)
                ap = encode_js(attr_or_prop)
                pr = encode_js(partial)
                "Drab.update_#{type}(#{sel}, #{ap}, #{new_value_of_attribute}, #{pr})"
              else
                nil
              end
            end
          end
        {:script, pattern, exprs, assigns_in_ampere} ->
          if has_common?(assigns_in_ampere, assigns_to_update_keys) do
            hash_and_value = Enum.map(exprs, fn hash ->
              {:expr, expr, _} = Drab.Live.Cache.get(hash)
              safe = eval_expr(expr, modules, updated_assigns)
              new_value = safe_to_string(safe)

              {hash, new_value}
            end)
            new_script = replace_pattern(pattern, hash_and_value) |> encode_js()
            "Drab.update_script(#{encode_js(selector)}, #{new_script}, #{encode_js(partial)})"
          else
            nil
          end
        _ -> raise "Ampere \"#{ampere_hash}\" can't be found in Drab Cache"
        # _ -> nil
      end
    end |> List.flatten() |> Enum.filter(&(&1))

    assign_updates = assign_updates_js(assigns_to_update, partial)
    all_javascripts = (assign_updates ++ update_javascripts) |> Enum.uniq()

    IO.inspect(all_javascripts)

    # IO.inspect :os.system_time(:microsecond) - t1
    {:ok, _} = function.(socket, all_javascripts |> Enum.join(";"))
    # IO.inspect :os.system_time(:microsecond) - t1

    # Save updated assigns in the Drab Server
    assigns_to_update = for {k, v} <- assigns_to_update, into: %{} do
      {Atom.to_string(k), v}
    end
    updated_assigns = Map.merge(current_assigns, assigns_to_update)
    # IO.inspect current_assigns
    # IO.inspect assigns_to_update
    # IO.inspect updated_assigns

    priv = socket |> Drab.pid() |> Drab.get_priv()
    # partial_assigns = priv.__ampere_assigns[partial]
    partial_assigns_updated = %{priv.__ampere_assigns | partial => updated_assigns}
    socket |> Drab.pid() |> Drab.set_priv(%{priv | __ampere_assigns: partial_assigns_updated})

    IO.inspect :os.system_time(:microsecond) - t1

    socket
  end

  defp replace_pattern(pattern, []), do: pattern
  defp replace_pattern(pattern, [{hash, value} | rest]) do
    new_pattern = String.replace(pattern, ~r/{{{{@drab-ampere:[^@}]+@drab-expr-hash:#{hash}}}}}/,
      to_string(value),
      global: true)
    replace_pattern(new_pattern, rest)
  end

  defp has_common?(list1, list2) do
    if Enum.find(list1, fn xa ->
      Enum.find(list2, fn xb -> xa == xb end)
    end), do: true, else: false
  end

  defp eval_expr(expr, modules, updated_assigns) do
    {safe, _assigns} = expr_with_imports(expr, modules)
      |> Code.eval_quoted([assigns: updated_assigns])
    safe
  end

  defp expr_with_imports(expr, modules) do
    #TODO: find it in the web.ex
    {view, router_helpers, error_helpers, gettext} = modules
    quote do
      import unquote(view)
      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
      use Phoenix.HTML
      import unquote(router_helpers)
      import unquote(error_helpers)
      import unquote(gettext)
      unquote(expr)
    end
  end

  # #TODO: refactor, not very efficient
  # defp assigns_for_expr(assigns_in_poke, assigns_in_expr, assigns_in_page) do
  #   # assigns_in_expr = String.split(assigns_in_expr) |> Enum.map(&String.to_existing_atom/1)
  #   missing_keys = assigns_in_expr -- Map.keys(assigns_in_poke)
  #   assigns_in_page = for {k, v} <- assigns_in_page, into: %{}, do: {String.to_existing_atom(k), v}
  #   stored_assigns = Enum.filter(assigns_in_page, fn {k, _} -> Enum.member?(missing_keys, k) end) |> Map.new()
  #   Map.merge(stored_assigns, assigns_in_poke) |> Map.to_list()
  # end

  defp assign_updates_js(assigns, partial) do
    Enum.map(assigns, fn {k, v} ->
      "__drab.assigns[#{Drab.Core.encode_js(partial)}][#{Drab.Core.encode_js(k)}] = #{Drab.Core.encode_js(v)}"
    end)
  end

  defp safe_to_encoded_js(safe), do: safe |> safe_to_string() |> encode_js()

  defp safe_to_string(list) when is_list(list), do: Enum.map(list, &safe_to_string/1) |> Enum.join("")
  defp safe_to_string({:safe, _} = safe), do: Phoenix.HTML.safe_to_string(safe)
  defp safe_to_string(safe), do: to_string(safe)

  defp assigns(socket, partial) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__ampere_assigns)
      |> Map.get(partial)
  end

  defp amperes(socket, partial) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__amperes)
      |> Map.get(partial)
  end

  defp index(socket) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__index)
  end

  defp partial_hash(socket, partial_name) do
    Drab.Live.Cache.get({:partial, partial_path(socket, partial_name)})
  end

  defp partial_path(socket, partial_name) do
    templates_path(socket) <> partial_name <> Drab.Config.drab_extension()
  end

  defp templates_path(socket) do
    view = Drab.get_view(socket)
    {path, _, _} = view.__templates__()
    path <> "/"
  end
end

defmodule Drab.Live do
  @moduledoc """
  Drab Module to provide a live access and update of assigns of the template, which is currently rendered and displayed
  in the browser.

  The idea is to reuse your Phoenix templates and let them live, to make a possibility to update assigns
  on the living page, from the Elixir, without reloading the whole stuff.

  Use `peek/2` to get the assign value, and `poke/2` to modify it directly in the displayed DOM tree.

  Drab.Live uses the modified EEx Engine (`Drab.Live.EExEngine`) to compile the template and indicate where assigns
  were rendered. To enable it, rename the template you want to go live from extension `.eex` to `.drab`. Then,
  add Drab Engine to the template engines in `config.exs`:

      config :phoenix, :template_engines,
        drab: Drab.Live.Engine

  ### Update Behaviours
  There are different behaviours of `Drab.Live`, depends on where the expression with the updated assign lives.
  For example, if the expression defines tag attribute, like `<span class="<%= @class %>">`, we don't want to
  re-render the whole tag, as it might override changes you made with other Drab module, or even with Javascript.
  Because of this, Drab finds the tag and updates only the required attributes.

  #### Plain Text
  If the expression in the template is given in any tag body, Drab will replace only the required part of the html.
  Consider the template, with initial value of `1` (given in render function in the Controller, as usual):

Chapter <%= @chapter_no %>.

  It renders to:

Chapter 1.

  This `<span>` over the expression is injected automatically by `Drab.Live.EExEngine`.
  Updating the `@chapter_no` attribute in the Drab Commander, using `poke/2`:

      chapter = peek(socket, :chapter_no)     # get the current value of `@chapter_no`
      poke(socket, chapter_no: chapter + 1)   # push the new value to the browser

  The above will change the `innerHTML` of the `<span drab-ampere=someid>` to "2":

Chapter 2.

  #### Attributes
  When the expression is defining the attribute of the tag, the behaviour if different. Let's assume there is
  a template with following html, rendered in the Controller with value of `@button` set to string `"btn-danger"`.

      <button class="btn <%= @button %>">

  It renders to:

      <button drab-ampere=someid class="btn btn-danger">

  Again, you can see injected `drab-ampere` attribute. This allows Drab to indicate where to update the attribute.
  Pushing the changes to the browser with:

      poke socket, button: "btn btn-info"

  will result with updated `class` attribute on the given tag. It is acomplished by rendering and running
  `node.setAttribute("class", "btn btn-info")`.

  Notice that the pattern where your expression lives is preserved: you may update only the partials of the
  attribute value string.

  #### Properties
  Nowadays we deal more with Node proporties than attributes. This is why `Drab.Live` introduces the special syntax.
  When using the @ sign at the beginning of the attribute name, it will be treated as a property.

      <button @hidden=<%= @hidden %>>

  Updating `@hidden` in the Drab Commander with `poke/2` will change the value of the `hidden` property
  (without dollar sign!), by sending the update javascript, like `node['hidden'] = false`.

  You may also dig deeper into the Node properties, using dot - like in JavaScript - to bind the expression
  with the specific property. The good example is to set up `.style`:

      <button @style.backgroundColor="<%= @color %>">

  Aditially, Drab sets up all the properties defined that way when the page loads. Thanks to this, you
  don't have to worry about the initial value.

  Notice that `@property=<%= expression %>` *is the only available syntax*, you can not use string pattern or
  give more than one expression. Property must be solid bind to the expression.

  The expression binded with the property *must be encodable to JS*, so, for example, tuples are not allowed here.
  Please refer to `Poison` for more information about encoding JS.

  #### Scripts
  When the assign we want to change is inside the `<script></script>` tag, Drab will re-evaluate the whole
  script after assigment change. Let's say you don't want to use `$property=<%=expression%>` syntax to define
  the object property. You may want to render the javascript:

  If you render the template in the Controller with `@button_state` set to `false`, the initial html will look like:

  Again, Drab injects some ID to know where to find its victim. After you `poke/2` the new value of `@button_state`,
  Drab will re-render the whole script with a new value and will send a request to re-evaluate the script.
  Browser will run something like: `eval("document.querySelectorAll(\"button\").hidden = true")`.

  ### Partials
  Function `poke/2` and `peek/2` works on the default template - the one rendered with the Controller. In case there
  are some child templates, rendered inside the main one, you need to specify the template name as a second argument
  of `poke/3` and `peek/3` functions.

  Assigns are archored within their partials. Manipulation of the assign outside the template it lives will raise
  `ArgumentError` in case of `poke`, or return `nil` when `peeking' the value.

  ### Limitions
  Because Drab must interpret the template, inject it's ID etc, it assumes that the template HTML is valid.
  There are also some limits for defining attributes, properties, etc. See `Drab.Live.EExEngine` for a full
  description.
  """
  import Drab.Core
  require IEx

  use DrabModule
  @doc false
  def js_templates(),  do: ["drab.events.js", "drab.live.js"]

  @doc false
  def transform_payload(payload, _state) do
    payload |> Map.put_new("value", payload["val"])
  end

  @doc false
  def transform_socket(socket, payload, state) do
    # store assigns in Drab Server
    priv = Map.merge(state.priv, %{
      __ampere_assigns: payload["__assigns"],
      __amperes: payload["__amperes"],
      __index:   payload["__index"]
    })
    Drab.pid(socket) |> Drab.set_priv(priv)
    socket
  end

  @doc """
  Returns the current value of the assign from the main partial or `nil` if not found.

      iex> peek(socket, :count)
      42
      iex> peek(socket, :nonexistent)
      nil
  """
  #TODO: think if it is needed to sign/encrypt
  def peek(socket, assign), do: peek(socket, nil, assign)

  @doc """
  Like `peek/2`, but takes partial name and returns assign from that specified partial.

  The value is take from the current

      iex> peek(socket, "users.html", :count)
      42
      iex> peek(socket, :count)
      nil

  """
  def peek(socket, partial, assign) when is_binary(assign) do
    hash = if partial, do: partial_hash(socket, partial), else: index(socket)
    assigns(socket, hash)[assign]
  end
  def peek(socket, partial, assign) when is_atom(assign), do: peek(socket, partial, Atom.to_string(assign))

  @doc """
  Updates the current page in the browser with the new assigns value.

  Returns untouched socket.

      iex> poke(socket, count: 42)
      %Phoenix.Socket{ ...
  """
  def poke(socket, assigns) do
    do_poke(socket, index(socket), assigns, &Drab.Core.exec_js/2)
  end

  @doc """
  Like `poke/2`, but limited only to the given partial name.

  Returns untouched socket.

      iex> poke(socket, "user.html", name: "Bożywój")
      %Phoenix.Socket{ ...
  """
  def poke(socket, partial, assigns) do
    hash = partial_hash(socket, partial)
    do_poke(socket, hash, assigns, &Drab.Core.exec_js/2)
  end

  @doc """
  The same as `poke/2`, but broadcasts the changes instead of pushing it to the current browser.

  See `Drab.Commander.broadcasting/1` for broadcasting options.

  Returns untouched socket.
  """
  def poke!(socket, assigns) do
    do_poke(socket, index(socket), assigns, &Drab.Core.broadcast_js/2)
  end

  @doc """
  Like `poke!/2`, but limited only to the given partial name.

  Raises `ArgumentError` when assign is not found within the partial.

  Returns untouched socket.

      iex> poke!(socket, "user.html", name: "Bożywój")
      %Phoenix.Socket{ ...
  """
  def poke!(socket, partial, assigns) do
    hash = partial_hash(socket, partial)
    do_poke(socket, hash, assigns, &Drab.Core.broadcast_js/2)
  end

  defp do_poke(socket, partial, assigns, function) do
    #TODO: improve perfomance. Now it takes 10 ms
    t1 = :os.system_time(:microsecond)
    # IO.inspect :os.system_time(:microsecond) - t1

    # partial = partial || index(socket)

    current_assigns = assigns(socket, partial)
    current_assigns_keys = Map.keys(current_assigns) |> Enum.map(&String.to_existing_atom/1)
    assigns_to_update = Enum.into(assigns, %{})
    assigns_to_update_keys = Map.keys(assigns_to_update)

    for as <- assigns_to_update_keys do
      unless Enum.find(current_assigns_keys, fn key -> key === as end) do
        raise ArgumentError, message: """
          Assign @#{as} not found in Drab EEx Template

          Please make sure all proper assigns have been set. If this
          is a child template, ensure assigns are given explicitly by
          the parent template as they are not automatically forwarded.

          Available assigns:
          #{inspect current_assigns_keys}
          """
      end
    end

    updated_assigns = current_assigns
      |> Enum.map(fn {k, v} -> {String.to_existing_atom(k), v} end)
      |> Keyword.merge(assigns)

    app_module = Drab.Config.app_module()
    modules = {
      socket.assigns.__controller.__drab__().view,
      Module.concat(app_module, Router.Helpers),
      Module.concat(app_module, ErrorHelpers),
      Module.concat(app_module, Gettext)
    }

    # TODO: check only amperes which contains the changed assigns
    amperes = amperes(socket, partial)

    # construct the javascripts for update of amperes
    #TODO: group updates on one node
    update_javascripts = for ampere_hash <- amperes do
      selector = "[drab-ampere='#{ampere_hash}']"

      case Drab.Live.Cache.get(ampere_hash) do
        {:expr, expr, assigns_in_expr} ->
          # change only if poked assign exist in this ampere
          #TODO: stay DRY
          if has_common?(assigns_in_expr, assigns_to_update_keys) do
            safe = eval_expr(expr, modules, updated_assigns)
            new_value = safe_to_encoded_js(safe)

            "Drab.update_drab_span(#{encode_js(selector)}, #{new_value}, #{encode_js(partial)})"
          else
            nil
          end
        {:attribute, list} ->
          for {type, attr_or_prop, pattern, exprs, assigns_in_ampere} <- list do
            if Regex.match?(~r/{{{{@drab-ampere:[^@}]+@drab-expr-hash:[^@}]+}}}}/, attr_or_prop) do
              #TODO: special form, without atribute name
              # ignored for now, let's think if it needs to be covered
              # warning appears during compile-time
              nil
            else
              if has_common?(assigns_in_ampere, assigns_to_update_keys) do
                evaluated_expressions = Enum.map(exprs, fn hash ->
                  {:expr, expr, _} = Drab.Live.Cache.get(hash)
                  new_value = eval_expr(expr, modules, updated_assigns)
                  # new_value = safe_to_string(safe)
                  # new_value = safe
                  {hash, new_value}
                end)
                new_value_of_attribute = case type do
                  # update in pattern
                  :attr ->
                    replace_pattern(pattern, evaluated_expressions) |> encode_js()
                  :prop ->
                    {_, new_value} = evaluated_expressions |> List.first()
                    new_value |> encode_js()
                end
                sel = encode_js(selector)
                ap = encode_js(attr_or_prop)
                pr = encode_js(partial)
                "Drab.update_#{type}(#{sel}, #{ap}, #{new_value_of_attribute}, #{pr})"
              else
                nil
              end
            end
          end
        {:script, pattern, exprs, assigns_in_ampere} ->
          if has_common?(assigns_in_ampere, assigns_to_update_keys) do
            hash_and_value = Enum.map(exprs, fn hash ->
              {:expr, expr, _} = Drab.Live.Cache.get(hash)
              safe = eval_expr(expr, modules, updated_assigns)
              new_value = safe_to_string(safe)

              {hash, new_value}
            end)
            new_script = replace_pattern(pattern, hash_and_value) |> encode_js()
            "Drab.update_script(#{encode_js(selector)}, #{new_script}, #{encode_js(partial)})"
          else
            nil
          end
        _ -> raise "Ampere \"#{ampere_hash}\" can't be found in Drab Cache"
        # _ -> nil
      end
    end |> List.flatten() |> Enum.filter(&(&1))

    assign_updates = assign_updates_js(assigns_to_update, partial)
    all_javascripts = (assign_updates ++ update_javascripts) |> Enum.uniq()

    IO.inspect(all_javascripts)

    # IO.inspect :os.system_time(:microsecond) - t1
    {:ok, _} = function.(socket, all_javascripts |> Enum.join(";"))
    # IO.inspect :os.system_time(:microsecond) - t1

    # Save updated assigns in the Drab Server
    assigns_to_update = for {k, v} <- assigns_to_update, into: %{} do
      {Atom.to_string(k), v}
    end
    updated_assigns = Map.merge(current_assigns, assigns_to_update)
    # IO.inspect current_assigns
    # IO.inspect assigns_to_update
    # IO.inspect updated_assigns

    priv = socket |> Drab.pid() |> Drab.get_priv()
    # partial_assigns = priv.__ampere_assigns[partial]
    partial_assigns_updated = %{priv.__ampere_assigns | partial => updated_assigns}
    socket |> Drab.pid() |> Drab.set_priv(%{priv | __ampere_assigns: partial_assigns_updated})

    IO.inspect :os.system_time(:microsecond) - t1

    socket
  end

  defp replace_pattern(pattern, []), do: pattern
  defp replace_pattern(pattern, [{hash, value} | rest]) do
    new_pattern = String.replace(pattern, ~r/{{{{@drab-ampere:[^@}]+@drab-expr-hash:#{hash}}}}}/,
      to_string(value),
      global: true)
    replace_pattern(new_pattern, rest)
  end

  defp has_common?(list1, list2) do
    if Enum.find(list1, fn xa ->
      Enum.find(list2, fn xb -> xa == xb end)
    end), do: true, else: false
  end

  defp eval_expr(expr, modules, updated_assigns) do
    {safe, _assigns} = expr_with_imports(expr, modules)
      |> Code.eval_quoted([assigns: updated_assigns])
    safe
  end

  defp expr_with_imports(expr, modules) do
    #TODO: find it in the web.ex
    {view, router_helpers, error_helpers, gettext} = modules
    quote do
      import unquote(view)
      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
      use Phoenix.HTML
      import unquote(router_helpers)
      import unquote(error_helpers)
      import unquote(gettext)
      unquote(expr)
    end
  end

  # #TODO: refactor, not very efficient
  # defp assigns_for_expr(assigns_in_poke, assigns_in_expr, assigns_in_page) do
  #   # assigns_in_expr = String.split(assigns_in_expr) |> Enum.map(&String.to_existing_atom/1)
  #   missing_keys = assigns_in_expr -- Map.keys(assigns_in_poke)
  #   assigns_in_page = for {k, v} <- assigns_in_page, into: %{}, do: {String.to_existing_atom(k), v}
  #   stored_assigns = Enum.filter(assigns_in_page, fn {k, _} -> Enum.member?(missing_keys, k) end) |> Map.new()
  #   Map.merge(stored_assigns, assigns_in_poke) |> Map.to_list()
  # end

  defp assign_updates_js(assigns, partial) do
    Enum.map(assigns, fn {k, v} ->
      "__drab.assigns[#{Drab.Core.encode_js(partial)}][#{Drab.Core.encode_js(k)}] = #{Drab.Core.encode_js(v)}"
    end)
  end

  defp safe_to_encoded_js(safe), do: safe |> safe_to_string() |> encode_js()

  defp safe_to_string(list) when is_list(list), do: Enum.map(list, &safe_to_string/1) |> Enum.join("")
  defp safe_to_string({:safe, _} = safe), do: Phoenix.HTML.safe_to_string(safe)
  defp safe_to_string(safe), do: to_string(safe)

  defp assigns(socket, partial) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__ampere_assigns)
      |> Map.get(partial)
  end

  defp amperes(socket, partial) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__amperes)
      |> Map.get(partial)
  end

  defp index(socket) do
    socket
      |> Drab.pid()
      |> Drab.get_priv()
      |> Map.get(:__index)
  end

  defp partial_hash(socket, partial_name) do
    Drab.Live.Cache.get({:partial, partial_path(socket, partial_name)})
  end

  defp partial_path(socket, partial_name) do
    templates_path(socket) <> partial_name <> Drab.Config.drab_extension()
  end

  defp templates_path(socket) do
    view = Drab.get_view(socket)
    {path, _, _} = view.__templates__()
    path <> "/"
  end
end


Run code formatter on ExUnit.CallbacksTest

Code.require_file("../test_helper.exs", __DIR__)

defmodule ExUnit.CallbacksTest do
  use ExUnit.Case

  import ExUnit.CaptureIO

  def start_counter(_) do
    [counter: []]
  end

  test "callbacks run custom code with context" do
    defmodule CallbacksTest do
      use ExUnit.Case

      setup_all do
        [context: :setup_all]
      end

      setup do
        %{initial_setup: true}
      end

      setup context do
        assert context[:initial_setup]
        assert context[:context] == :setup_all
        {:ok, [context: :setup]}
      end

      test "callbacks", context do
        assert context[:context] == :setup
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run() end) =~ "1 test, 0 failures"
  end

  test "named callbacks run custom code in order" do
    defmodule NamedCallbacksTest do
      use ExUnit.Case

      import ExUnit.CallbacksTest
      setup_all :start_counter

      setup :store_1
      setup [:store_2, :store_3]

      setup context do
        [counter: [4 | context.counter]]
      end

      setup :store_5

      test "callbacks", context do
        assert context[:counter] == [5, 4, 3, 2, 1]
      end

      defp store(context, number), do: [counter: [number | context.counter]]
      defp store_1(context), do: store(context, 1)
      defp store_2(context), do: store(context, 2)
      defp store_3(context), do: store(context, 3)
      defp store_5(context), do: store(context, 5)
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run() end) =~ "1 test, 0 failures"
  end

  test "doesn't choke on setup errors" do
    defmodule SetupTest do
      use ExUnit.Case

      setup _ do
        :ok = error()
      end

      test "ok" do
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()

    assert capture_io(fn -> ExUnit.run() end) =~
             "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke on setup_all errors" do
    defmodule SetupAllTest do
      use ExUnit.Case

      setup_all _ do
        :ok = error()
      end

      test "ok" do
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()

    assert capture_io(fn -> ExUnit.run() end) =~
             "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke on dead supervisor" do
    defmodule StartSupervisedErrorTest do
      use ExUnit.Case

      @tag timeout: 500
      test "ok" do
        start_supervised({
          Task,
          fn ->
            Process.flag(:trap_exit, true)
            Process.sleep(:infinity)
          end
        })
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run() end) =~ "supervisor shutdown timed out after 500ms"
  end

  test "doesn't choke on on_exit errors" do
    defmodule OnExitErrorTest do
      use ExUnit.Case

      test "ok" do
        on_exit(fn -> :ok = error() end)
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()

    assert capture_io(fn -> ExUnit.run() end) =~
             "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke when on_exit exits" do
    defmodule OnExitExitTest do
      use ExUnit.Case, async: false

      test "ok" do
        on_exit(fn -> Process.exit(self(), :kill) end)
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run() end) =~ ">) killed"
  end

  defp no_formatters! do
    ExUnit.configure(formatters: [])
    on_exit(fn -> ExUnit.configure(formatters: [ExUnit.CLIFormatter]) end)
  end

  test "exits with shutdown reason" do
    defmodule OnExitAliveTest do
      use ExUnit.Case

      setup do
        parent = self()

        pid =
          spawn_link(fn ->
            Process.flag(:trap_exit, true)
            send(parent, :ready)

            receive do
              {:EXIT, ^parent, :shutdown} ->
                receive do: ({:on_exit, pid} -> send(pid, :done))
            end
          end)

        receive do: (:ready -> :ok)

        on_exit(fn ->
          send(pid, {:on_exit, self()})
          assert_receive :done
          IO.puts("on_exit run")
        end)

        :ok
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run() end)
    assert output =~ "on_exit run"
    assert output =~ "1 test, 0 failures"
  end

  test "runs multiple on_exit exits and overrides by ref" do
    defmodule OnExitSuccessTest do
      use ExUnit.Case

      setup do
        on_exit(fn ->
          IO.puts("on_exit setup run")
        end)

        on_exit({:overridden, 1}, fn ->
          IO.puts("on_exit 1 overridden -> not run")
        end)

        :ok
      end

      setup_all do
        on_exit(fn ->
          IO.puts("on_exit setup_all run")
        end)

        :ok
      end

      test "ok" do
        on_exit(fn ->
          IO.puts("simple on_exit run")
        end)

        on_exit({:overridden, 2}, fn ->
          IO.puts("on_exit 2 overridden -> not run")
        end)

        on_exit({:overridden, 2}, fn ->
          IO.puts("on_exit 2 overrides -> run")
        end)

        on_exit({:overridden, 1}, fn ->
          IO.puts("on_exit 1 overrides -> run")
        end)

        :ok
      end
    end

    no_formatters!()
    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run() end)

    expected = """
    on_exit 2 overrides -> run
    simple on_exit run
    on_exit 1 overrides -> run
    on_exit setup run
    on_exit setup_all run
    """

    assert output =~ expected
    refute output =~ "not run"
  end

  test "runs multiple on_exit on failure" do
    defmodule OnExitFailureTest do
      use ExUnit.Case

      setup do
        on_exit(fn ->
          IO.puts("on_exit setup run")
        end)

        :ok
      end

      setup_all do
        on_exit(fn ->
          IO.puts("on_exit setup_all run")
        end)

        :ok
      end

      test "ok" do
        on_exit(fn ->
          IO.puts("simple on_exit run")
        end)

        flunk("oops")
      end
    end

    no_formatters!()
    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run() end)

    expected = """
    simple on_exit run
    on_exit setup run
    on_exit setup_all run
    """

    assert output =~ expected
  end

  test "raises an error when setting an invalid callback in setup" do
    defmodule SetupErrorTest do
      use ExUnit.Case

      setup do
        {:ok, "foo"}
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()

    expected =
      "** (RuntimeError) expected ExUnit callback in " <>
        "ExUnit.CallbacksTest.SetupErrorTest to return " <>
        ":ok | keyword | map, got {:ok, \"foo\"} instead"

    assert capture_io(fn -> ExUnit.run() end) =~ expected
  end

  test "raises an error when overriding a reserved callback key in setup" do
    defmodule SetupReservedTest do
      use ExUnit.Case

      setup do
        {:ok, file: "foo"}
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()

    expected =
      "** (RuntimeError) ExUnit callback in " <>
        "ExUnit.CallbacksTest.SetupReservedTest is " <>
        "trying to set reserved field :file to \"foo\""

    assert capture_io(fn -> ExUnit.run() end) =~ expected
  end
end

defmodule ExUnit.CallbacksNoTests do
  use ExUnit.Case, async: true

  setup_all do
    raise "never run"
  end

  setup do
    raise "never run"
  end
end

Code.require_file "../test_helper.exs", __DIR__

defmodule ExUnit.CallbacksTest do
  use ExUnit.Case

  import ExUnit.CaptureIO

  def start_counter(_) do
    [counter: []]
  end

  test "callbacks run custom code with context" do
    defmodule CallbacksTest do
      use ExUnit.Case

      setup_all do
        [context: :setup_all]
      end

      setup do
        %{initial_setup: true}
      end

      setup context do
        assert context[:initial_setup]
        assert context[:context] == :setup_all
        {:ok, [context: :setup]}
      end

      test "callbacks", context do
        assert context[:context] == :setup
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "1 test, 0 failures"
  end

  test "named callbacks run custom code in order" do
    defmodule NamedCallbacksTest do
      use ExUnit.Case

      import ExUnit.CallbacksTest
      setup_all :start_counter

      setup :store_1
      setup [:store_2, :store_3]

      setup context do
        [counter: [4 | context.counter]]
      end

      setup :store_5

      test "callbacks", context do
        assert context[:counter] == [5, 4, 3, 2, 1]
      end

      defp store(context, number), do: [counter: [number | context.counter]]
      defp store_1(context), do: store(context, 1)
      defp store_2(context), do: store(context, 2)
      defp store_3(context), do: store(context, 3)
      defp store_5(context), do: store(context, 5)
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "1 test, 0 failures"
  end

  test "doesn't choke on setup errors" do
    defmodule SetupTest do
      use ExUnit.Case

      setup _ do
        :ok = error()
      end

      test "ok" do
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke on setup_all errors" do
    defmodule SetupAllTest do
      use ExUnit.Case

      setup_all _ do
        :ok = error()
      end

      test "ok" do
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke on dead supervisor" do
    defmodule StartSupervisedErrorTest do
      use ExUnit.Case

      @tag timeout: 500
      test "ok" do
        start_supervised {Task, fn -> Process.flag(:trap_exit, true); Process.sleep(:infinity) end}
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~ "supervisor shutdown timed out after 500ms"
  end

  test "doesn't choke on on_exit errors" do
    defmodule OnExitErrorTest do
      use ExUnit.Case

      test "ok" do
        on_exit fn -> :ok = error() end
        :ok
      end

      defp error, do: :error
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "** (MatchError) no match of right hand side value: :error"
  end

  test "doesn't choke when on_exit exits" do
    defmodule OnExitExitTest do
      use ExUnit.Case, async: false

      test "ok" do
        on_exit fn -> Process.exit(self(), :kill) end
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           ">) killed"
  end

  defp no_formatters! do
    ExUnit.configure(formatters: [])
    on_exit fn -> ExUnit.configure(formatters: [ExUnit.CLIFormatter]) end
  end

  test "exits with shutdown reason" do
    defmodule OnExitAliveTest do
      use ExUnit.Case

      setup do
        parent = self()

        pid = spawn_link fn ->
          Process.flag(:trap_exit, true)
          send parent, :ready
          receive do
            {:EXIT, ^parent, :shutdown} ->
              receive do: ({:on_exit, pid} -> send pid, :done)
          end
        end

        receive do: (:ready -> :ok)

        on_exit fn ->
          send pid, {:on_exit, self()}
          assert_receive :done
          IO.puts "on_exit run"
        end

        :ok
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run end)
    assert output =~ "on_exit run"
    assert output =~ "1 test, 0 failures"
  end

  test "runs multiple on_exit exits and overrides by ref" do
    defmodule OnExitSuccessTest do
      use ExUnit.Case

      setup do
        on_exit fn ->
          IO.puts "on_exit setup run"
        end

        on_exit {:overridden, 1}, fn ->
          IO.puts "on_exit 1 overridden -> not run"
        end

        :ok
      end

      setup_all do
        on_exit fn ->
          IO.puts "on_exit setup_all run"
        end

        :ok
      end

      test "ok" do
        on_exit fn ->
          IO.puts "simple on_exit run"
        end

        on_exit {:overridden, 2}, fn ->
          IO.puts "on_exit 2 overridden -> not run"
        end

        on_exit {:overridden, 2}, fn ->
          IO.puts "on_exit 2 overrides -> run"
        end

        on_exit {:overridden, 1}, fn ->
          IO.puts "on_exit 1 overrides -> run"
        end

        :ok
      end
    end

    no_formatters!()
    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run end)

    assert output =~ """
    on_exit 2 overrides -> run
    simple on_exit run
    on_exit 1 overrides -> run
    on_exit setup run
    on_exit setup_all run
    """

    refute output =~ "not run"
  end

  test "runs multiple on_exit on failure" do
    defmodule OnExitFailureTest do
      use ExUnit.Case

      setup do
        on_exit fn ->
          IO.puts "on_exit setup run"
        end

        :ok
      end

      setup_all do
        on_exit fn ->
          IO.puts "on_exit setup_all run"
        end

        :ok
      end

      test "ok" do
        on_exit fn ->
          IO.puts "simple on_exit run"
        end

        flunk "oops"
      end
    end

    no_formatters!()
    ExUnit.Server.modules_loaded()
    output = capture_io(fn -> ExUnit.run end)

    assert output =~ """
    simple on_exit run
    on_exit setup run
    on_exit setup_all run
    """
  end

  test "raises an error when setting an invalid callback in setup" do
    defmodule SetupErrorTest do
      use ExUnit.Case

      setup do
        {:ok, "foo"}
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "** (RuntimeError) expected ExUnit callback in " <>
           "ExUnit.CallbacksTest.SetupErrorTest to return " <>
           ":ok | keyword | map, got {:ok, \"foo\"} instead"
  end

  test "raises an error when overriding a reserved callback key in setup" do
    defmodule SetupReservedTest do
      use ExUnit.Case

      setup do
        {:ok, file: "foo"}
      end

      test "ok" do
        :ok
      end
    end

    ExUnit.Server.modules_loaded()
    assert capture_io(fn -> ExUnit.run end) =~
           "** (RuntimeError) ExUnit callback in " <>
           "ExUnit.CallbacksTest.SetupReservedTest is " <>
           "trying to set reserved field :file to \"foo\""
  end
end

defmodule ExUnit.CallbacksNoTests do
  use ExUnit.Case, async: true

  setup_all do
    raise "never run"
  end

  setup do
    raise "never run"
  end
end


Allow specifying firmware path with firmware.unpack

`mix firmware.unpack` would only look for a firmware that was currently built
for a project. Sometimes you may have multiple firmware files to inspect that
were built elsewhere and this would be a convenient tool to inspect them.

This adds `--fw path/to/.fw` support for those cases. It also changes the default
outpath to be the firmware file name with `.unpacked` appended. (i.e. `hello_nerves.fw -> hello_nerves.unpacked`)


Ensure that the proper build is loaded as last_build

defmodule BaumeisterWeb.Builds do

  @moduledoc """
  This is a bounded context `Builds` for accessing projects together
  with their builds. Essentially, it abstracts the way of joining
  projects and builds instead of accessing both directly
  via an Ecto.Repo from a controller.

  TODO:

  * [x] Project as in Builds.Project with cached `last_build` values. This
    used for Showing a simple project list (`list_projects()`).
  * [x] Project with embedded Build-Schema.  Used for the list of all builds
    of a project. Read-only. Created by joining in-memory the builds of a
    project.
  * [ ] Build with a Log-Entry. Used for the detailed view of a single build.
  * [x] Create/Update Build of a project, used by BuildListener. Calculates
    the cached `last_build` values.

  """

  import Ecto.Query, only: [from: 2]
  alias BaumeisterWeb.Repo
  require Logger

  alias BaumeisterWeb.Builds.Project
  alias BaumeisterWeb.Builds.Build
  alias BaumeisterWeb.ProjectBridge
  alias BaumeisterWeb.Web.Project, as: WP
  alias BaumeisterWeb.Web.Build, as: WB
  alias Ecto.Changeset
  alias Ecto.Multi
  alias Baumeister.BuildEvent

  @doc """
  Inserts the project into the database and the baumeister coordinator.
  It is excpected that `last_build` is empty and that no `builds` are
  embedded: A new project has no build yet.
  """
  @spec insert_project(Changeset.t) :: {:ok, Project.t} | {:error, any}
  def insert_project(p_changeset) do
    changeset = validate_no_values(p_changeset, [:last_build, :builds])
    # Make a downcast from Project to WP and insert the data
    do_insert_project(changeset)
  end

  @spec do_insert_project(Changeset.t) :: {:ok, Project.t} | {:error, Changeset.t}
  defp do_insert_project(%Changeset{errors: []} = p_changeset) do
    changeset = WP.changeset(%WP{}, p_changeset.changes)
    case Repo.insert(changeset) do
      {:ok, project} ->
        case ProjectBridge.add_project_to_coordinator(project) do
          :ok -> enabled = project.enabled
                 ^enabled = ProjectBridge.set_status(project)
                 {:ok, convert_up project}
          {:error, msg} ->
            Logger.error("Error inserting project into core: #{inspect changeset}")
            Repo.delete!(project)
            {:error, Ecto.Changeset.add_error(%{changeset | action: :insert}, :name, msg)}
        end
      {:error, new_changeset} -> {:error, new_changeset}
    end
  end
  defp do_insert_project(p_changeset) do
    {:error, p_changeset}
  end

  @doc """
  Validates that the given fields have no values or do not exist in the changeset.
  """
  @spec validate_no_values(Changeset.t, atom | [atom]) :: Changeset.t
  def validate_no_values(changeset, field) when is_atom(field),
    do: validate_no_values(changeset, [field])
  def validate_no_values(changeset, fields) do
    fields
    |> Enum.reduce(changeset, fn f, chs ->
        case Changeset.fetch_field(chs, f) do
          :error -> chs # field is not there
          {:data, :nil} -> chs # embeds_one with no value (default)
          {:data, []} -> chs # embeds_may with no values (default)
          {origin, v} when origin in [:data, :changes] ->
            m = "Error: Field #{inspect f} has value #{inspect v} but no value allowed"
            Logger.error m
            Changeset.add_error(%{chs | action: :insert}, :name, m)
        end
      end)
  end

  @doc """
  Converts the structs from lower abstraction into their counterparts
  of the Builds Context.
  """
  @spec convert_up(WP.t|WB.t) :: Project.t | Build.t
  defp convert_up(p = %WP{}) do
    fields = [:name, :url, :plugins, :enabled, :delay, :id,
      :updated_at, :inserted_at]
    |> Enum.map(fn f -> {f, Map.get(p, f)} end)
    new_p = struct!(%Project{}, fields)
    if is_nil(p.last_build_id) do
      new_p
    else
      # Logger.debug "convert_up: load last_build #{p.last_build_id}"
      last_build = Repo.get_by(WB, [number: p.last_build_id, project_id: p.id]) |> convert_up
      # Logger.debug "last_build is #{inspect last_build}"
      %Project{new_p | last_build: last_build}
    end
  end
  defp convert_up(b = %WB{}) do
    fields = [:number, :log, :coordinate, :config, :status, :id,
      :updated_at, :inserted_at, :project_id]
    |> Enum.map(fn f -> {f, Map.get(b, f)} end)
    struct!(%Build{}, fields)
  end
  defp convert_up(nil), do: nil

  @doc """
  Returns all projects.
  """
  @spec list_projects() :: [Project.t]
  def list_projects() do
    Repo.all(WP) |> Enum.map &convert_up/1
  end

  @doc """
  Returns all builds belonging to the project.
  """
  @spec builds_for_project(Project.t) :: [Build.t]
  def builds_for_project(project) do
    q = from(b in Build,
        where: b.project_id == ^project.id,
        order_by: [desc: b.number])
    Repo.all(q)
  end

  @doc """
  Gets a project by its id.
  """
  @spec get_project(integer) :: Project
  def get_project(id) when is_integer(id) do
    Repo.get!(WP, id)
    |> convert_up()
  end

  @doc """
  Creates a new build from the build event and stores into the database.
  The project is updated accordingly. If the referenced project does
  not exist, the call fails.
  """
  @spec create_build_from_event(BuildEvent.t) :: {:ok, any} | {:error, any}
  def create_build_from_event(ev = %BuildEvent{build_counter: counter, coordinate: coord}) do
    Logger.debug("create_build_from_event with ev=#{inspect ev}")
    project = Repo.get_by!(WP, name: coord.project_name)
    Logger.debug("Found project: #{inspect project}")
    build_changeset = project
    |> create_build(counter)
    |> WB.changeset(summerize_build_event(ev))
    Logger.debug("build changeset: #{inspect build_changeset}")
    project_changeset = WP.changeset(project, %{last_build_id: counter})
    {:ok, b} = Repo.insert_or_update(build_changeset)
    {:ok, _p} = Repo.update(project_changeset)
    {:ok, convert_up b}
  end

  @doc """
  Creates or retrieves an `Build` for the given `project` and the given
  `build_counter`. If there already exists a build entity in the database,
  it is returned otherwise a new build struct is created.
  """
  @spec create_build(WP.t, integer) :: Build.t
  defp create_build(project = %WP{}, build_counter) do
    case Repo.get_by(WB, [project_id: project.id, number: build_counter]) do
      nil -> %WB{project_id: project.id, number: build_counter, id: nil}
      build -> build
    end
  end

  @doc """
  Transforms a build event to a build struct to used in a changeset.
  """
  @spec summerize_build_event(BuildEvent.t) :: %{atom => any}
  defp summerize_build_event(build_event = %BuildEvent{coordinate: coord}) do
    [coordinate: "#{inspect coord}",
      status: status(build_event),
      log: log(build_event)]
    |> Enum.reject(fn {_k, v} -> v == nil end)
    |> Enum.into(%{})
  end

  @doc """
  Converts the status from the build event to the build struct
  """
  defp status(%BuildEvent{action: nil}), do: 0
  defp status(%BuildEvent{action: :start}), do: 1
  defp status(%BuildEvent{action: :result, data: :ok}), do: 2
  defp status(%BuildEvent{action: :result}), do: 3
  defp status(_), do: nil

  defp log(%BuildEvent{action: :log, data: log_data}), do: log_data
  defp log(_), do: nil

end

defmodule BaumeisterWeb.Builds do

  @moduledoc """
  This is a bounded context `Builds` for accessing projects together
  with their builds. Essentially, it abstracts the way of joining
  projects and builds instead of accessing both directly
  via an Ecto.Repo from a controller.

  TODO:

  * [x] Project as in Builds.Project with cached `last_build` values. This
    used for Showing a simple project list (`list_projects()`).
  * [x] Project with embedded Build-Schema.  Used for the list of all builds
    of a project. Read-only. Created by joining in-memory the builds of a
    project.
  * [ ] Build with a Log-Entry. Used for the detailed view of a single build.
  * [x] Create/Update Build of a project, used by BuildListener. Calculates
    the cached `last_build` values.

  """

  import Ecto.Query, only: [from: 2]
  alias BaumeisterWeb.Repo
  require Logger

  alias BaumeisterWeb.Builds.Project
  alias BaumeisterWeb.Builds.Build
  alias BaumeisterWeb.ProjectBridge
  alias BaumeisterWeb.Web.Project, as: WP
  alias BaumeisterWeb.Web.Build, as: WB
  alias Ecto.Changeset
  alias Ecto.Multi
  alias Baumeister.BuildEvent

  @doc """
  Inserts the project into the database and the baumeister coordinator.
  It is excpected that `last_build` is empty and that no `builds` are
  embedded: A new project has no build yet.
  """
  @spec insert_project(Changeset.t) :: {:ok, Project.t} | {:error, any}
  def insert_project(p_changeset) do
    changeset = validate_no_values(p_changeset, [:last_build, :builds])
    # Make a downcast from Project to WP and insert the data
    do_insert_project(changeset)
  end

  @spec do_insert_project(Changeset.t) :: {:ok, Project.t} | {:error, Changeset.t}
  defp do_insert_project(%Changeset{errors: []} = p_changeset) do
    changeset = WP.changeset(%WP{}, p_changeset.changes)
    case Repo.insert(changeset) do
      {:ok, project} ->
        case ProjectBridge.add_project_to_coordinator(project) do
          :ok -> enabled = project.enabled
                 ^enabled = ProjectBridge.set_status(project)
                 {:ok, convert_up project}
          {:error, msg} ->
            Logger.error("Error inserting project into core: #{inspect changeset}")
            Repo.delete!(project)
            {:error, Ecto.Changeset.add_error(%{changeset | action: :insert}, :name, msg)}
        end
      {:error, new_changeset} -> {:error, new_changeset}
    end
  end
  defp do_insert_project(p_changeset) do
    {:error, p_changeset}
  end

  @doc """
  Validates that the given fields have no values or do not exist in the changeset.
  """
  @spec validate_no_values(Changeset.t, atom | [atom]) :: Changeset.t
  def validate_no_values(changeset, field) when is_atom(field),
    do: validate_no_values(changeset, [field])
  def validate_no_values(changeset, fields) do
    fields
    |> Enum.reduce(changeset, fn f, chs ->
        case Changeset.fetch_field(chs, f) do
          :error -> chs # field is not there
          {:data, :nil} -> chs # embeds_one with no value (default)
          {:data, []} -> chs # embeds_may with no values (default)
          {origin, v} when origin in [:data, :changes] ->
            m = "Error: Field #{inspect f} has value #{inspect v} but no value allowed"
            Logger.error m
            Changeset.add_error(%{chs | action: :insert}, :name, m)
        end
      end)
  end

  @doc """
  Converts the structs from lower abstraction into their counterparts
  of the Builds Context.
  """
  @spec convert_up(WP.t|WB.t) :: Project.t | Build.t
  defp convert_up(p = %WP{}) do
    fields = [:name, :url, :plugins, :enabled, :delay, :id, :updated_at, :inserted_at]
    |> Enum.map(fn f -> {f, Map.get(p, f)} end)
    new_p = struct!(%Project{}, fields)
    if is_nil(p.last_build_id) do
      new_p
    else
      last_build = Repo.get(WB, p.last_build_id) |> convert_up
      %Project{new_p | last_build: last_build}
    end
  end
  defp convert_up(b = %WB{}) do
    fields = [:number, :log, :coordinate, :config, :status, :id,
      :updated_at, :inserted_at, :project_id]
    |> Enum.map(fn f -> {f, Map.get(b, f)} end)
    struct!(%Build{}, fields)
  end
  defp convert_up(nil), do: nil

  @doc """
  Returns all projects.
  """
  @spec list_projects() :: [Project.t]
  def list_projects() do
    Repo.all(WP) |> Enum.map &convert_up/1
  end

  @doc """
  Returns all builds belonging to the project.
  """
  @spec builds_for_project(Project.t) :: [Build.t]
  def builds_for_project(project) do
    q = from(b in Build,
        where: b.project_id == ^project.id,
        order_by: [desc: b.number])
    Repo.all(q)
  end

  def get_project(id) when is_integer(id) do
    Repo.get!(WP, id)
    |> convert_up()
  end

  @doc """
  Creates a new build from the build event and stores into the database.
  The project is updated accordingly. If the referenced project does
  not exist, the call fails.
  """
  @spec create_build_from_event(BuildEvent.t) :: {:ok, any} | {:error, any}
  def create_build_from_event(ev = %BuildEvent{build_counter: counter, coordinate: coord}) do
    Logger.debug("create_build_from_event with ev=#{inspect ev}")
    project = Repo.get_by!(WP, name: coord.project_name)
    Logger.debug("Found project: #{inspect project}")
    build_changeset = project
    |> create_build(counter)
    |> WB.changeset(summerize_build_event(ev))
    Logger.debug("build changeset: #{inspect build_changeset}")
    project_changeset = WP.changeset(project, %{last_build_id: counter})
    {:ok, b} = Repo.insert_or_update(build_changeset)
    {:ok, _p} = Repo.update(project_changeset)
    {:ok, convert_up b}
  end

  @doc """
  Creates or retrieves an `Build` for the given `project` and the given
  `build_counter`. If there already exists a build entity in the database,
  it is returned otherwise a new build struct is created.
  """
  @spec create_build(WP.t, integer) :: Build.t
  defp create_build(project = %WP{}, build_counter) do
    case Repo.get_by(WB, [project_id: project.id, number: build_counter]) do
      nil -> %WB{project_id: project.id, number: build_counter, id: nil}
      build -> build
    end
  end

  @doc """
  Transforms a build event to a build struct to used in a changeset.
  """
  @spec summerize_build_event(BuildEvent.t) :: %{atom => any}
  defp summerize_build_event(build_event = %BuildEvent{coordinate: coord}) do
    [coordinate: "#{inspect coord}",
      status: status(build_event),
      log: log(build_event)]
    |> Enum.reject(fn {_k, v} -> v == nil end)
    |> Enum.into(%{})
  end

  @doc """
  Converts the status from the build event to the build struct
  """
  defp status(%BuildEvent{action: nil}), do: 0
  defp status(%BuildEvent{action: :start}), do: 1
  defp status(%BuildEvent{action: :result, data: :ok}), do: 2
  defp status(%BuildEvent{action: :result}), do: 3
  defp status(_), do: nil

  defp log(%BuildEvent{action: :log, data: log_data}), do: log_data
  defp log(_), do: nil

end


Update docs of transaction registry

Update docs of transaction registry

defmodule Appsignal.TransactionRegistry do
  @moduledoc """

  Internal module which keeps a registry of the transaction handles
  linked to their originating process.

  This is used on various places to link a calling process to its transaction.
  For instance, the `Appsignal.ErrorHandler` module uses it to be able to
  complete the transaction in case the originating process crashed.

  The transactions are stored in an ETS table (with
  `{:write_concurrency, true}`, so no bottleneck is created); and the
  originating process is monitored to clean up the ETS table when the
  process has finished.

  """

  use GenServer

  require Logger

  @table :'$appsignal_transaction_registry'

  alias Appsignal.Transaction

  @spec start_link :: {:ok, pid}
  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc """
  Register the current process as the owner of the given transaction.
  """
  @spec register(Transaction.t) :: :ok
  def register(transaction) do
    pid = self()
    true = :ets.insert(@table, {pid, transaction})
    GenServer.cast(__MODULE__, {:monitor, pid})
  end

  @doc """
  Given a process ID, return its associated transaction.
  """
  @spec lookup(pid, boolean) :: Transaction.t) | nil
  def lookup(pid, return_removed \\ false) do
    case :ets.lookup(@table, pid) do
      [{^pid, :removed}] ->
        case return_removed do
          false -> nil
          true -> :removed
        end
      [{^pid, transaction}] -> transaction
      [] -> nil
    end
  end

  @doc """
  Unregister the current process as the owner of the given transaction.
  """
  @spec remove_transaction(Transaction.t) :: :ok | {:error, :not_found}
  def remove_transaction(%Transaction{} = transaction) do
    GenServer.call(__MODULE__, {:remove, transaction})
  end

  ##

  defmodule State do
    @moduledoc false
    defstruct table: nil
  end

  def init([]) do
    table = :ets.new(@table, [:set, :named_table,
                              {:keypos, 1}, :public, {:write_concurrency, true}])
    {:ok, %State{table: table}}
  end

  def handle_call({:remove, transaction}, _from, state) do
    reply =
      case :ets.match(@table, {:'$1', transaction}) do
        [[pid] | _] ->
          true = :ets.delete(@table, pid)
          true = :ets.insert(@table, {pid, :removed})
          Process.send_after(self(), {:delete, pid}, 5000)
          :ok
        [] ->
          {:error, :not_found}
      end
    {:reply, reply, state}
  end

  def handle_cast({:monitor, pid}, state) do
    Process.monitor(pid)
    {:noreply, state}
  end

  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    # we give the error handler some time to process the error report
    Process.send_after(self(), {:delete, pid}, 5000)
    {:noreply, state}
  end

  def handle_info({:delete, pid}, state) do
    :ets.delete(@table, pid)
    {:noreply, state}
  end

  def handle_info(msg, state) do
    {:noreply, state}
  end

end

defmodule Appsignal.TransactionRegistry do
  @moduledoc """

  Internal module which keeps a registry of the transaction handles
  linked to their originating process.

  This is used on various places to link a calling process to its transaction.
  For instance, the `Appsignal.ErrorHandler` module uses it to be able to
  complete the transaction in case the originating process crashed.

  The transactions are stored in an ETS table (with
  `{:write_concurrency, true}`, so no bottleneck is created); and the
  originating process is monitored to clean up the ETS table when the
  process has finished.

  """

  use GenServer

  require Logger

  @table :'$appsignal_transaction_registry'

  alias Appsignal.Transaction

  @spec start_link :: {:ok, pid}
  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @spec register(Transaction.transaction) :: :ok
  def register(transaction) do
    pid = self()
    true = :ets.insert(@table, {pid, transaction})
    GenServer.cast(__MODULE__, {:monitor, pid})
  end

  @spec register(Transaction.transaction) :: :ok
  def lookup(pid, return_removed \\ false) do
    case :ets.lookup(@table, pid) do
      [{^pid, :removed}] ->
        case return_removed do
          false -> nil
          true -> :removed
        end
      [{^pid, transaction}] -> transaction
      [] -> nil
    end
  end

  def remove_transaction(%Transaction{} = transaction) do
    GenServer.call(__MODULE__, {:remove, transaction})
  end

  ##

  defmodule State do
    @moduledoc false
    defstruct table: nil
  end

  def init([]) do
    table = :ets.new(@table, [:set, :named_table,
                              {:keypos, 1}, :public, {:write_concurrency, true}])
    {:ok, %State{table: table}}
  end

  def handle_call({:remove, transaction}, _from, state) do
    reply =
      case :ets.match(@table, {:'$1', transaction}) do
        [[pid] | _] ->
          true = :ets.delete(@table, pid)
          true = :ets.insert(@table, {pid, :removed})
          Process.send_after(self(), {:delete, pid}, 5000)
          :ok
        [] ->
          {:error, :not_found}
      end
    {:reply, reply, state}
  end

  def handle_cast({:monitor, pid}, state) do
    Process.monitor(pid)
    {:noreply, state}
  end

  def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
    # we give the error handler some time to process the error report
    Process.send_after(self(), {:delete, pid}, 5000)
    {:noreply, state}
  end

  def handle_info({:delete, pid}, state) do
    :ets.delete(@table, pid)
    {:noreply, state}
  end

  def handle_info(msg, state) do
    {:noreply, state}
  end

end


add documentation to NodeConnect

add documentation to NodeConnect

#
# The MIT License (MIT)
#
# Copyright (c) 2014 Undead Labs, LLC
#

defmodule Discovery.Handler.NodeConnect do
  @moduledoc """
  A poller handler which will instruct the `Discovery.NodeConnector` to attempt to
  establish an OTP connection to the node of found services.

  Consul service definitions exposing a special tag containing their OTP name will be passed
  to `Discovery.NodeConnector` and all other nodes providing services which do not contain
  that tag will be ignored.

  The tag is a colon (`:`) separated string where the first element is the key `otp_name` and
  the second element is a string representation of the OTP node name.

  ### Example Definition

      {
        "service": {
          "name": "my_application",
          "check": {
            "ttl": "15s"
          },
          "tags": [
            "otp_name:my_application@jamie.undeadlabs.com"
          ]
        }
      }
  """

  use Discovery.Handler.Behaviour
  alias Discovery.Directory
  alias Discovery.Service

  @passing "passing"
  @warning "warning"

  @doc """
  Connect to a list of services. Only services marked passing will be connected to.
  """
  @spec connect([Service.t]) :: :ok
  def connect([]), do: :ok
  def connect([%Service{name: name, status: status} = service|rest]) when status in [@passing, @warning] do
    case otp_name(service) do
      nil ->
        {:error, :no_node_name}
      otp_name ->
        Discovery.NodeConnector.connect(otp_name, name)
    end
    connect(rest)
  end
  def connect([_|rest]), do: connect(rest)

  @doc """
  Disconnect from a list of stale services. A list of stale services can be obtained
  by calling `NodeConnect.stale/1` with a list of `Discovery.Service`.
  """
  @spec disconnect([{binary, [atom]}]) :: :ok
  def disconnect([]), do: :ok
  def disconnect([{service, nodes}|rest]) do
    _disconnect(service, nodes)
    disconnect(rest)
  end
  defp _disconnect(_, []), do: :ok
  defp _disconnect(service, [node|rest]) do
    Discovery.NodeConnector.disconnect(node, service)
    _disconnect(service, rest)
  end

  @doc """
  Returns a list of stale registered nodes in `Discovery.Directory` which are no longer
  providing a service given the list of fresh services.

  The return value is a list of tuples where the first element is a service name and
  the second element is a list of node names which are providing that service.

  ### Example

      [{"router", [:"reset@undead"]}]
  """
  def stale(given) do
    service_names = Enum.map(given, fn(%Service{name: name}) -> name end) |> Enum.uniq
    grouped       = for name <- service_names, do: {name, Directory.nodes(name)}
    Enum.reduce(grouped, [], fn({name, nodes}, drop) ->
      {_, unknown} = Enum.partition(nodes, fn(node) ->
        Enum.any?(given, fn
          %Service{name: sname} = service when sname == name ->
            otp_name(service) == node
          _ ->
            false
        end)
      end)

      [{name, unknown}|drop]
    end)
  end

  #
  # Discovery.Handler.Behaviour callbacks
  #

  def handle_services(services, state) do
    stale(services) |> disconnect
    connect(services)
    {:ok, state}
  end

  #
  # Private
  #

  defp otp_name(%{tags: []}), do: nil
  defp otp_name(%{tags: tags}) when is_list(tags) do
    case Keyword.get(tags, :otp_name) do
      nil ->
        nil
      name ->
        String.to_atom(name)
    end
  end
  defp otp_name(_), do: nil
end

#
# The MIT License (MIT)
#
# Copyright (c) 2014 Undead Labs, LLC
#

defmodule Discovery.Handler.NodeConnect do
  @moduledoc """
  A poller handler which will instruct the `Discovery.NodeConnector` to attempt to
  establish an OTP connection to the node of found services.

  Consul service definitions exposing a special tag containing their OTP name will be passed
  to `Discovery.NodeConnector` and all other nodes providing services which do not contain
  that tag will be ignored.

  The tag is a colon (`:`) separated string where the first element is the key `otp_name` and
  the second element is a string representation of the OTP node name.

  ### Example Definition

      {
        "service": {
          "name": "my_application",
          "check": {
            "ttl": "15s"
          },
          "tags": [
            "otp_name:my_application@jamie.undeadlabs.com"
          ]
        }
      }
  """

  use Discovery.Handler.Behaviour
  alias Discovery.Directory
  alias Discovery.Service

  @passing "passing"
  @warning "warning"

  def connect([]), do: :ok
  def connect([%Service{name: name, status: status} = service|rest]) when status in [@passing, @warning] do
    case otp_name(service) do
      nil ->
        {:error, :no_node_name}
      otp_name ->
        Discovery.NodeConnector.connect(otp_name, name)
    end
    connect(rest)
  end
  def connect([_|rest]), do: connect(rest)

  def disconnect([]), do: :ok
  def disconnect([{service, nodes}|rest]) do
    _disconnect(service, nodes)
    disconnect(rest)
  end
  defp _disconnect(_, []), do: :ok
  defp _disconnect(service, [node|rest]) do
    Discovery.NodeConnector.disconnect(node, service)
    _disconnect(service, rest)
  end

  @doc """
  Returns a list of stale registered nodes in `Discovery.Directory` which are no longer
  providing a service given the list of fresh services.

  The return value is a list of tuples where the first element is a service name and
  the second element is a list of node names which are providing that service.

  ### Example

      [{"router", [:"reset@undead"]}]
  """
  def stale(given) do
    service_names = Enum.map(given, fn(%Service{name: name}) -> name end) |> Enum.uniq
    grouped       = for name <- service_names, do: {name, Directory.nodes(name)}
    Enum.reduce(grouped, [], fn({name, nodes}, drop) ->
      {_, unknown} = Enum.partition(nodes, fn(node) ->
        Enum.any?(given, fn
          %Service{name: sname} = service when sname == name ->
            otp_name(service) == node
          _ ->
            false
        end)
      end)

      [{name, unknown}|drop]
    end)
  end

  #
  # Discovery.Handler.Behaviour callbacks
  #

  def handle_services(services, state) do
    stale(services) |> disconnect
    connect(services)
    {:ok, state}
  end

  #
  # Private
  #

  defp otp_name(%{tags: []}), do: nil
  defp otp_name(%{tags: tags}) when is_list(tags) do
    case Keyword.get(tags, :otp_name) do
      nil ->
        nil
      name ->
        String.to_atom(name)
    end
  end
  defp otp_name(_), do: nil
end


Remove unused format (#174)

Remove unused format (#174)

defmodule Gateway.Changeset.Validator.Settings do
  @moduledoc """
  This module provides helpers to validate individual plugin settings via JSON Schema that is stored inside methods.
  """
  alias Ecto.Changeset
  import Ecto.Changeset
  import Gateway.Changeset.Validator.JsonSchema

  # JWT
  def validate_settings(%Changeset{changes: %{name: "jwt", settings: settings}} = ch) do
    {%{}, %{signature: :string}}
    |> cast(settings, [:signature])
    |> validate_required([:signature])
    |> put_changeset_errors(ch)
  end

  # ACL
  def validate_settings(%Changeset{changes: %{name: "acl"}} = ch) do
    validate_via_json_schema(ch, :settings, %{
      "type" => "object",
      "required" => ["rules"],
      "additionalProperties" => false,
      "properties" => %{
        "rules" => %{
          "type" => "array",
          "minItems" => 1,
          "uniqueItems" => true,
          "items" => %{
            "type" => "object",
            "required" => ["methods", "path", "scopes"],
            "properties" => %{
              "methods" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string",
                  "enum" => ["GET", "POST", "PUT", "DELETE", "PATCH"]
                }
              },
              "path" => %{
                "type" => "string"
              },
              "scopes" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string"
                }
              }
            }
          }
        }
      }
    })
  end

  # Validator
  def validate_settings(%Changeset{changes: %{name: "validator"}} = ch) do
    validate_via_json_schema(ch, :settings, %{
      "type" => "object",
      "required" => ["rules"],
      "additionalProperties" => false,
      "properties" => %{
        "rules" => %{
          "type" => "array",
          "minItems" => 1,
          "uniqueItems" => true,
          "items" => %{
            "type" => "object",
            "required" => ["methods", "path", "schema"],
            "properties" => %{
              "methods" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string",
                  "enum" => ["POST", "PUT", "PATCH"]
                }
              },
              "path" => %{
                "type" => "string"
              },
              "schema" => %{
                "type" => "object"
              }
            }
          }
        }
      }
    })
  end

  # IPRestriction
  def validate_settings(%Changeset{changes: %{name: "ip_restriction"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "anyOf" => [
        %{"required" => ["whitelist", "blacklist"]},
        %{"required" => ["whitelist"]},
        %{"required" => ["blacklist"]}
      ],
      "additionalProperties" => false,
      "properties" => %{
        "whitelist" => %{
          "type" => "array",
          "items" => %{
            "type" => "string",
            "oneOf" => [
              %{"pattern" => "^(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)$"}
            ]
          }
        },
        "blacklist" => %{
          "type" => "array",
          "items" => %{
            "type" => "string",
            "oneOf" => [
              %{"pattern" => "^(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)$"}
            ]
          }
        }
      }
    })
  end

  # Proxy
  def validate_settings(%Changeset{changes: %{name: "proxy"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "required" => ["host"],
      "additionalProperties" => false,
      "properties" => %{
        "strip_api_path" => %{
          "type" => "boolean"
        },
        "additional_headers" => %{
          "type" => "array",
          "uniqueItems" => true,
          "items" => %{
            "type" => "string"
          }
        },
        "scheme" => %{
          "enum" => ["http", "https"]
        },
        "host" => %{
          "type" => "string",
          "oneOf" => [
            %{"format" => "hostname"},
            %{"format" => "ipv4"}
          ]
        },
        "port" => %{
          "type" => "integer"
        },
        "path" => %{
          "type" => "string",
          "pattern" => "^/.*"
        },
        "strip_api_path" => %{
          "type" => "boolean"
        },
        "additional_headers" => %{
          "type" => "array",
          "items" => %{
            "type" => "object"
          }
        }
      },
    })
  end

  # Scopes
  def validate_settings(%Changeset{changes: %{name: "scopes"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "required" => ["strategy"],
      "additionalProperties" => false,
      "properties" => %{
        "strategy" => %{
          "enum" => ["pcm", "jwt"]
        },
        "url_template" => %{
          "type" => "string"
        }
      }
    })
  end

  # general
  def validate_settings(ch), do: ch

  defp put_changeset_errors(%Changeset{valid?: true}, ch), do: ch
  defp put_changeset_errors(%Changeset{valid?: false, errors: errors}, ch) do
    ch
    |> Map.merge(%{errors: errors, valid?: false})
  end
end

defmodule Gateway.Changeset.Validator.Settings do
  @moduledoc """
  This module provides helpers to validate individual plugin settings via JSON Schema that is stored inside methods.
  """
  alias Ecto.Changeset
  import Ecto.Changeset
  import Gateway.Changeset.Validator.JsonSchema

  # JWT
  def validate_settings(%Changeset{changes: %{name: "jwt", settings: settings}} = ch) do
    {%{}, %{signature: :string}}
    |> cast(settings, [:signature])
    |> validate_required([:signature])
    |> put_changeset_errors(ch)
  end

  # ACL
  def validate_settings(%Changeset{changes: %{name: "acl"}} = ch) do
    validate_via_json_schema(ch, :settings, %{
      "type" => "object",
      "required" => ["rules"],
      "additionalProperties" => false,
      "properties" => %{
        "rules" => %{
          "type" => "array",
          "minItems" => 1,
          "uniqueItems" => true,
          "items" => %{
            "type" => "object",
            "required" => ["methods", "path", "scopes"],
            "properties" => %{
              "methods" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string",
                  "enum" => ["GET", "POST", "PUT", "DELETE", "PATCH"]
                }
              },
              "path" => %{
                "type" => "string",
                "pattern": "^/.*$"
              },
              "scopes" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string"
                }
              }
            }
          }
        }
      }
    })
  end

  # Validator
  def validate_settings(%Changeset{changes: %{name: "validator"}} = ch) do
    validate_via_json_schema(ch, :settings, %{
      "type" => "object",
      "required" => ["rules"],
      "additionalProperties" => false,
      "properties" => %{
        "rules" => %{
          "type" => "array",
          "minItems" => 1,
          "uniqueItems" => true,
          "items" => %{
            "type" => "object",
            "required" => ["methods", "path", "schema"],
            "properties" => %{
              "methods" => %{
                "type" => "array",
                "minItems" => 1,
                "items" => %{
                  "type" => "string",
                  "enum" => ["POST", "PUT", "PATCH"]
                }
              },
              "path" => %{
                "type" => "string"
              },
              "schema" => %{
                "type" => "object"
              }
            }
          }
        }
      }
    })
  end

  # IPRestriction
  def validate_settings(%Changeset{changes: %{name: "ip_restriction"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "anyOf" => [
        %{"required" => ["whitelist", "blacklist"]},
        %{"required" => ["whitelist"]},
        %{"required" => ["blacklist"]}
      ],
      "additionalProperties" => false,
      "properties" => %{
        "whitelist" => %{
          "type" => "array",
          "items" => %{
            "type" => "string",
            "oneOf" => [
              %{"pattern" => "^(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)$"}
            ]
          }
        },
        "blacklist" => %{
          "type" => "array",
          "items" => %{
            "type" => "string",
            "oneOf" => [
              %{"pattern" => "^(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)\.(\\*|\\d+)$"}
            ]
          }
        }
      }
    })
  end

  # Proxy
  def validate_settings(%Changeset{changes: %{name: "proxy"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "required" => ["host"],
      "additionalProperties" => false,
      "properties" => %{
        "strip_api_path" => %{
          "type" => "boolean"
        },
        "additional_headers" => %{
          "type" => "array",
          "uniqueItems" => true,
          "items" => %{
            "type" => "string"
          }
        },
        "scheme" => %{
          "enum" => ["http", "https"]
        },
        "host" => %{
          "type" => "string",
          "oneOf" => [
            %{"format" => "hostname"},
            %{"format" => "ipv4"}
          ]
        },
        "port" => %{
          "type" => "integer"
        },
        "path" => %{
          "type" => "string",
          "pattern" => "^/.*"
        },
        "strip_api_path" => %{
          "type" => "boolean"
        },
        "additional_headers" => %{
          "type" => "array",
          "items" => %{
            "type" => "object"
          }
        }
      },
    })
  end

  # Scopes
  def validate_settings(%Changeset{changes: %{name: "scopes"}} = ch) do
    ch
    |> validate_via_json_schema(:settings, %{
      "type" => "object",
      "required" => ["strategy"],
      "additionalProperties" => false,
      "properties" => %{
        "strategy" => %{
          "enum" => ["pcm", "jwt"]
        },
        "url_template" => %{
          "type" => "string"
        }
      }
    })
  end

  # general
  def validate_settings(ch), do: ch

  defp put_changeset_errors(%Changeset{valid?: true}, ch), do: ch
  defp put_changeset_errors(%Changeset{valid?: false, errors: errors}, ch) do
    ch
    |> Map.merge(%{errors: errors, valid?: false})
  end
end


Be sure to expand rebar path before using it

defmodule Mix.Tasks.Deps.Compile do
  use Mix.Task

  @shortdoc "Compile dependencies"

  @moduledoc """
  Compile dependencies.

  By default, compile all dependencies that do not provide an
  app file yet. A list of deps can be given to force the compilation
  of specific deps or --force recompiling all deps.

  By default, it tries to detect if the project contains one of
  the following files:

  * `mix.exs`      - if so, invokes `mix compile`
  * `rebar.config` - if so, invokes `rebar compile`
  * `Makefile`     - if so, invokes `make`

  The compilation can be customized by passing a `compile` option
  in the dependency:

      { :some_dependency, "0.1.0", git: "...", compile: :compile_some_dependency }

  If the compile option is an atom, it will invoke the given atom
  in the current project passing the app name as argument. Except
  if the atom is `:noop`, where nothing is done.

  If a binary, it is considered to be command line instructions
  which mix will use to shell out.
  """

  import Mix.Deps, only: [all: 0, all: 1, by_name: 1, format_dep: 1, deps_path: 1, deps_path: 0]

  def run(args) do
    case OptionParser.parse(args) do
      { opts, [] } ->
        deps =
          if opts[:force] do
            all -- all(:unavailable)
          else
            all(:noappfile)
          end

        do_compile(deps)
      { _, tail } ->
        do_compile(by_name(tail))
    end
  end

  defp do_compile(deps) do
    shell = Mix.shell

    # Load available paths but still allow it to be called down the road
    Mix.Task.run "deps.loadpaths", ["--no-check"]
    Mix.Task.reenable "deps.loadpaths"

    Enum.each deps, fn(dep) ->
      Mix.Dep[app: app, status: status, opts: opts] = dep

      check_unavailable!(app, status)
      shell.info "* Compiling #{app}"

      root_path = File.expand_path(deps_path)

      compile_path = deps_path(dep)
      ebin = File.join(compile_path, "ebin") /> binary_to_list

      # Avoid compilation conflicts
      :code.del_path(ebin /> File.expand_path)

      File.cd! compile_path, fn ->
        cond do
          opts[:compile] -> do_command(opts[:compile], app)
          mix?           -> Mix.Project.in_subproject fn -> Mix.Task.run "compile", ["--no-check"] end
          rebar?         -> shell.info System.cmd("rebar compile deps_dir=#{inspect root_path}")
          make?          -> shell.info System.cmd("make")
          true           -> shell.error "Could not compile #{app}, no mix.exs, rebar.config or Makefile " <>
                             "(pass :compile as an option to customize compilation, set it to :noop to do nothing)"
        end
      end

      Code.prepend_path ebin
    end
  end

  defp mix?,   do: File.regular?("mix.exs")
  defp rebar?, do: File.regular?("rebar.config") or File.regular?("rebar.config.script")
  defp make?,  do: File.regular?("Makefile")

  defp check_unavailable!(app, { :unavailable, _ }) do
    raise Mix.Error, message: "Cannot compile dependency #{app} because " <>
      "it isn't available, run `mix deps.get` first"
  end

  defp check_unavailable!(_, _) do
    :ok
  end

  defp do_command(:noop, _) do
    :ok
  end

  defp do_command(atom, app) when is_atom(atom) do
    apply Mix.Project.current, atom, [app]
  end

  defp do_command(command, _) do
    Mix.shell.info System.cmd command
  end
end

defmodule Mix.Tasks.Deps.Compile do
  use Mix.Task

  @shortdoc "Compile dependencies"

  @moduledoc """
  Compile dependencies.

  By default, compile all dependencies that do not provide an
  app file yet. A list of deps can be given to force the compilation
  of specific deps or --force recompiling all deps.

  By default, it tries to detect if the project contains one of
  the following files:

  * `mix.exs`      - if so, invokes `mix compile`
  * `rebar.config` - if so, invokes `rebar compile`
  * `Makefile`     - if so, invokes `make`

  The compilation can be customized by passing a `compile` option
  in the dependency:

      { :some_dependency, "0.1.0", git: "...", compile: :compile_some_dependency }

  If the compile option is an atom, it will invoke the given atom
  in the current project passing the app name as argument. Except
  if the atom is `:noop`, where nothing is done.

  If a binary, it is considered to be command line instructions
  which mix will use to shell out.
  """

  import Mix.Deps, only: [all: 0, all: 1, by_name: 1, format_dep: 1, deps_path: 1, deps_path: 0]

  def run(args) do
    case OptionParser.parse(args) do
      { opts, [] } ->
        deps =
          if opts[:force] do
            all -- all(:unavailable)
          else
            all(:noappfile)
          end

        do_compile(deps)
      { _, tail } ->
        do_compile(by_name(tail))
    end
  end

  defp do_compile(deps) do
    shell = Mix.shell

    # Load available paths but still allow it to be called down the road
    Mix.Task.run "deps.loadpaths", ["--no-check"]
    Mix.Task.reenable "deps.loadpaths"

    Enum.each deps, fn(dep) ->
      Mix.Dep[app: app, status: status, opts: opts] = dep

      check_unavailable!(app, status)
      shell.info "* Compiling #{app}"

      compile_path = deps_path(dep)
      ebin = File.join(compile_path, "ebin") /> binary_to_list

      # Avoid compilation conflicts
      :code.del_path(ebin /> File.expand_path)

      File.cd! compile_path, fn ->
        cond do
          opts[:compile] -> do_command(opts[:compile], app)
          mix?           -> Mix.Project.in_subproject fn -> Mix.Task.run "compile", ["--no-check"] end
          rebar?         -> shell.info  System.cmd("rebar compile deps_dir=#{inspect deps_path}")
          make?          -> shell.info  System.cmd("make")
          true           -> shell.error "Could not compile #{app}, no mix.exs, rebar.config or Makefile " <>
                             "(pass :compile as an option to customize compilation, set it to :noop to do nothing)"
        end
      end

      Code.prepend_path ebin
    end
  end

  defp mix?,   do: File.regular?("mix.exs")
  defp rebar?, do: File.regular?("rebar.config") or File.regular?("rebar.config.script")
  defp make?,  do: File.regular?("Makefile")

  defp check_unavailable!(app, { :unavailable, _ }) do
    raise Mix.Error, message: "Cannot compile dependency #{app} because " <>
      "it isn't available, run `mix deps.get` first"
  end

  defp check_unavailable!(_, _) do
    :ok
  end

  defp do_command(:noop, _) do
    :ok
  end

  defp do_command(atom, app) when is_atom(atom) do
    apply Mix.Project.current, atom, [app]
  end

  defp do_command(command, _) do
    Mix.shell.info System.cmd command
  end
end