Cleanup function access in IEx.Autocomplete

defmodule IEx.Autocomplete do
  @moduledoc """
  Autocompletion for the Elixir shell.
  """

  defrecord Mod, type: nil, name: ''
  defrecord Fun, name: '', arities: []

  defprotocol Entry do
    def to_entries(entry)
    def match?(entry, hint)
  end

  defimpl Entry, for: Mod do
    def to_entries(mod) do
      if mod.type === :erlang do
        [':'++mod.name]
      else
        [mod.name]
      end
    end
    def match?(mod, hint) do
      :lists.prefix(hint, mod.name)
    end
  end

  defimpl Entry, for: Fun do
    def to_entries(fun) do
      lc a inlist fun.arities, do: '#{fun.name}/#{a}'
    end
    def match?(fun, hint) do
      :lists.prefix(hint, fun.name)
    end
  end

  def expand([]) do
    format_expansion root_modules++iex_helpers_exports
  end
  def expand([h|t]=expr) do
    cond do
      h === ?. ->
        expand_dot reduce(t)
      h === ?: ->
        expand_erlang_modules
      (h >= ?a and h <= ?z) or (h >= ?A and h <= ?Z) ->
        expand_expr reduce(expr)
      List.member?('(+[ ', h) ->
        expand ''
      true ->
        no_match
    end
  end

  defp expand_dot(expr) do
    case Code.string_to_ast expr do
      {:ok, atom} when is_atom(atom) ->
        expand_module_funs atom
      {:ok, {:__aliases__,_,list}} ->
        format_expansion elixir_module_subentries(list), ''
      {:ok, {{:.,_,[{:__aliases__,_,list},fun]},_,[]}} when is_atom(fun) ->
        expand_module_funs(elixir_module(list), atom_to_list(fun))
      _ -> no_match
    end
  end

  defp expand_expr(expr) do
    case Code.string_to_ast expr do
      {:ok, atom} when is_atom(atom) ->
          expand_erlang_modules atom_to_list(atom)
      {:ok, {:__aliases__,_,[root]}} ->
        hint = atom_to_list(root)
        entries = Enum.filter root_modules, fn m ->
          :lists.prefix(hint, m.name)
        end
        format_expansion entries, hint
      {:ok, {:__aliases__,_,list}} ->
        hint = atom_to_list(:lists.last(list))
        list = :lists.sublist(list, length(list)-1)
        format_expansion elixir_module_subentries(list, hint), hint
      {:ok, {{:., _, [mod,fun]},_,[]}} when is_atom(mod) and is_atom(fun) ->
        expand_module_funs mod, atom_to_list(fun)
      {:ok, {{:.,_,[{:__aliases__,_,list},fun]},_,[]}} when is_atom(fun) ->
        expand_module_funs(elixir_module(list), atom_to_list(fun))
      _ -> no_match
    end
  end

  defp reduce(expr) do
    last_token(List.reverse(expr), [' ', '(', '[', '+', '-'])
  end

  defp last_token(s, []) do
    s
  end
  defp last_token(s, [h|t]) do
    last_token(List.last(:string.tokens(s, h)), t)
  end

  defp no_match, do: {:no, '', []}

  defp format_expansion(list, hint // '')
  defp format_expansion([], _) do
    no_match
  end
  defp format_expansion([Fun[name: name]], hint) do
    {:yes, :lists.nthtail(length(hint), name), []}
  end
  defp format_expansion([Mod[name: name]], hint) do
    {:yes, :lists.nthtail(length(hint), name)++'.', []}
  end
  defp format_expansion([first|_]=entries, hint) do
    b = Enum.map entries, fn e -> list_to_binary(e.name) end
    lh = length hint
    n = :binary.longest_common_prefix(b)
    cond do
      n == 0 or (n == lh) ->
        {:yes, '',
         Enum.reduce entries, [], fn e, acc -> acc++Entry.to_entries(e) end}
      true ->
        {:yes, :lists.sublist(first.name, 1+lh, n-lh), []}
    end
  end

  ## Root Modules

  defp loaded_modules do
    Enum.map :code.all_loaded, fn({m,_}) -> m end
  end

  defp root_modules do
    Enum.reduce :code.all_loaded, [], fn {m,_}, acc ->
      mod = atom_to_list(m)
      if :lists.prefix('Elixir', mod) do
        tokens = :string.tokens(mod, '-')
        if length(tokens) === 2 do
          [Mod.new(name: :lists.last(tokens), type: :elixir)|acc]
        else
          acc
        end
      else
        [Mod.new(name: mod, type: :erlang)|acc]
      end
    end
  end

  ## Root Functions (exported in IEx.Helpers)

  defp iex_helpers_exports do
    filter = [{:__info__,1},{:module_info, 0},{:module_info, 1}]
    module_funs IEx.Helpers, filter
  end

  ## Erlang modules

  defp expand_erlang_modules(hint // '') do
    format_expansion match_erlang_modules(hint), hint
  end

  defp match_erlang_modules('') do
    Enum.filter root_modules, fn m -> m.type === :erlang end
  end
  defp match_erlang_modules(hint) do
    Enum.filter root_modules, fn m -> Entry.match?(m, hint) end
  end

  ## Elixir modules

  defp elixir_module([:Elixir|_]=list) do
    list_to_atom(:string.join(Enum.map(list, atom_to_list &1), '-'))
  end
  defp elixir_module(list) do
    elixir_module([:Elixir|list])
  end

  defp elixir_module_subentries(list, hint // '') do
    mod = elixir_module(list)
    if List.member?(loaded_modules, mod) do
      funs = Enum.filter module_funs(mod), fn Fun[name: name] ->
        :lists.prefix(hint, name)
      end
      elixir_module_submodules(mod, hint)++funs
    else
      []
    end
  end

  defp elixir_module_submodules(mod, hint) do
    modname = atom_to_list(mod)
    depth = length(:string.tokens(modname, '-'))+1
    base = :string.join([modname, hint], '-')
    Enum.reduce map_atom_to_list(loaded_modules), [], fn m, acc ->
      if m != base and :lists.prefix(base, m) do
        tokens = :string.tokens(m, '-')
        if length(tokens) == depth do
          name = :lists.nth(depth, tokens)
          [Mod.new(type: :elixir, name: name)|acc]
        else
          acc
        end
      else
        acc
      end
    end
  end

  ## Functions

  defp expand_module_funs(mod, hint // '')
  defp expand_module_funs(mod, '') do
    format_expansion module_funs(mod), ''
  end
  defp expand_module_funs(mod, hint) do
    entries = Enum.filter module_funs(mod), fn fun ->
      Entry.match?(fun, hint)
    end
    format_expansion entries, hint
  end

  defp module_funs(mod, filter // [{:__info__,1}]) do
    case :code.is_loaded(mod) do
      {:file, _} ->
        falist = mod.module_info(:exports)--filter
        list = Enum.reduce falist, [], fn {f,a}, acc ->
          case :lists.keyfind(f, 1, acc) do
            {f,aa} -> :lists.keyreplace(f, 1, acc, {f, [a|aa]})
            false -> [{f, [a]}|acc]
          end
        end
        lc {f, aa} inlist list, do: Fun.new(name: atom_to_list(f),
                                            arities: aa)
      _ -> []
    end
  end

  ## Generic Helpers

  defp map_atom_to_list(list) do
    Enum.map list, atom_to_list &1
  end
end

defmodule IEx.Autocomplete do
  @moduledoc """
  Autocompletion for the Elixir shell.
  """

  defrecord Mod, type: nil, name: ''
  defrecord Fun, name: '', arities: []

  defprotocol Entry do
    def to_entries(entry)
    def match?(entry, hint)
  end

  defimpl Entry, for: Mod do
    def to_entries(mod) do
      if mod.type === :erlang do
        [':'++mod.name]
      else
        [mod.name]
      end
    end
    def match?(mod, hint) do
      :lists.prefix(hint, mod.name)
    end
  end

  defimpl Entry, for: Fun do
    def to_entries(fun) do
      lc a inlist fun.arities, do: '#{fun.name}/#{a}'
    end
    def match?(fun, hint) do
      :lists.prefix(hint, fun.name)
    end
  end

  def expand([]) do
    format_expansion root_modules++iex_helpers_exports
  end
  def expand([h|t]=expr) do
    cond do
      h === ?. ->
        expand_dot reduce(t)
      h === ?: ->
        expand_erlang_modules
      (h >= ?a and h <= ?z) or (h >= ?A and h <= ?Z) ->
        expand_expr reduce(expr)
      List.member?('(+[ ', h) ->
        expand ''
      true ->
        no_match
    end
  end

  def expand_dot(expr) do
    case Code.string_to_ast expr do
      {:ok, atom} when is_atom(atom) ->
        expand_module_funs atom
      {:ok, {:__aliases__,_,list}} ->
        format_expansion elixir_module_subentries(list), ''
      {:ok, {{:.,_,[{:__aliases__,_,list},fun]},_,[]}} when is_atom(fun) ->
        expand_module_funs(elixir_module(list), atom_to_list(fun))
      _ -> no_match
    end
  end

  defp expand_expr(expr) do
    case Code.string_to_ast expr do
      {:ok, atom} when is_atom(atom) ->
          expand_erlang_modules atom_to_list(atom)
      {:ok, {:__aliases__,_,[root]}} ->
        hint = atom_to_list(root)
        entries = Enum.filter root_modules, fn m ->
          :lists.prefix(hint, m.name)
        end
        format_expansion entries, hint
      {:ok, {:__aliases__,_,list}} ->
        hint = atom_to_list(:lists.last(list))
        list = :lists.sublist(list, length(list)-1)
        format_expansion elixir_module_subentries(list, hint), hint
      {:ok, {{:., _, [mod,fun]},_,[]}} when is_atom(mod) and is_atom(fun) ->
        expand_module_funs mod, atom_to_list(fun)
      {:ok, {{:.,_,[{:__aliases__,_,list},fun]},_,[]}} when is_atom(fun) ->
        expand_module_funs(elixir_module(list), atom_to_list(fun))
      _ -> no_match
    end
  end

  defp reduce(expr) do
    last_token(List.reverse(expr), [' ', '(', '[', '+', '-'])
  end

  defp last_token(s, []) do
    s
  end
  defp last_token(s, [h|t]) do
    last_token(List.last(:string.tokens(s, h)), t)
  end

  defp no_match, do: {:no, '', []}

  defp format_expansion(list, hint // '')
  defp format_expansion([], _) do
    no_match
  end
  defp format_expansion([Fun[name: name]], hint) do
    {:yes, :lists.nthtail(length(hint), name), []}
  end
  defp format_expansion([Mod[name: name]], hint) do
    {:yes, :lists.nthtail(length(hint), name)++'.', []}
  end
  defp format_expansion([first|_]=entries, hint) do
    b = Enum.map entries, fn e -> list_to_binary(e.name) end
    lh = length hint
    n = :binary.longest_common_prefix(b)
    cond do
      n == 0 or (n == lh) ->
        {:yes, '',
         Enum.reduce entries, [], fn e, acc -> acc++Entry.to_entries(e) end}
      true ->
        {:yes, :lists.sublist(first.name, 1+lh, n-lh), []}
    end
  end

  ## Root Modules

  defp loaded_modules do
    Enum.map :code.all_loaded, fn({m,_}) -> m end
  end

  def root_modules do
    Enum.reduce :code.all_loaded, [], fn {m,_}, acc ->
      mod = atom_to_list(m)
      if :lists.prefix('Elixir', mod) do
        tokens = :string.tokens(mod, '-')
        if length(tokens) === 2 do
          [Mod.new(name: :lists.last(tokens), type: :elixir)|acc]
        else
          acc
        end
      else
        [Mod.new(name: mod, type: :erlang)|acc]
      end
    end
  end

  ## Root Functions (exported in IEx.Helpers)

  def iex_helpers_exports do
    filter = [{:__info__,1},{:module_info, 0},{:module_info, 1}]
    module_funs IEx.Helpers, filter
  end

  ## Erlang modules

  defp expand_erlang_modules(hint // '') do
    format_expansion match_erlang_modules(hint), hint
  end

  defp match_erlang_modules('') do
    Enum.filter root_modules, fn m -> m.type === :erlang end
  end
  defp match_erlang_modules(hint) do
    Enum.filter root_modules, fn m -> Entry.match?(m, hint) end
  end

  ## Elixir modules

  def elixir_module([:Elixir|_]=list) do
    list_to_atom(:string.join(Enum.map(list, atom_to_list &1), '-'))
  end
  def elixir_module(list) do
    elixir_module([:Elixir|list])
  end

  def elixir_module_subentries(list, hint // '') do
    mod = elixir_module(list)
    if List.member?(loaded_modules, mod) do
      funs = Enum.filter module_funs(mod), fn Fun[name: name] ->
        :lists.prefix(hint, name)
      end
      elixir_module_submodules(mod, hint)++funs
    else
      []
    end
  end

  def elixir_module_submodules(mod, hint) do
    modname = atom_to_list(mod)
    depth = length(:string.tokens(modname, '-'))+1
    base = :string.join([modname, hint], '-')
    Enum.reduce map_atom_to_list(loaded_modules), [], fn m, acc ->
      if m != base and :lists.prefix(base, m) do
        tokens = :string.tokens(m, '-')
        if length(tokens) == depth do
          name = :lists.nth(depth, tokens)
          [Mod.new(type: :elixir, name: name)|acc]
        else
          acc
        end
      else
        acc
      end
    end
  end

  ## Functions

  def expand_module_funs(mod, hint // '')
  def expand_module_funs(mod, '') do
    format_expansion module_funs(mod), ''
  end
  def expand_module_funs(mod, hint) do
    entries = Enum.filter module_funs(mod), fn fun ->
      Entry.match?(fun, hint)
    end
    format_expansion entries, hint
  end

  defp module_funs(mod, filter // [{:__info__,1}]) do
    case :code.is_loaded(mod) do
      {:file, _} ->
        falist = mod.module_info(:exports)--filter
        list = Enum.reduce falist, [], fn {f,a}, acc ->
          case :lists.keyfind(f, 1, acc) do
            {f,aa} -> :lists.keyreplace(f, 1, acc, {f, [a|aa]})
            false -> [{f, [a]}|acc]
          end
        end
        lc {f, aa} inlist list, do: Fun.new(name: atom_to_list(f),
                                            arities: aa)
      _ -> []
    end
  end

  ## Generic Helpers

  defp map_atom_to_list(list) do
    Enum.map list, atom_to_list &1
  end
end


Accepts binary for sweep_interval parameter (#45)

* Remove part of #34 since the original issue has been fixed in guardian

* code cleaning

* Also accepts binary values for sweep_interval. May be useful when configuring application with environment variables

defmodule GuardianDb.ExpiredSweeper do
  @moduledoc """
  Periocially purges expired tokens from the DB.

  ## Example
    config :guardian_db, GuardianDb,
      sweep_interval: 60 # 1 hour

    # in your supervisor
      worker(GuardianDb.ExpiredSweeper, [])
  """
  use GenServer

  def start_link, do: start_link([])

  def start_link(state, _opts \\ []) do
    state = Enum.into(state, %{})

    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  Reset the purge timer.
  """
  def reset_timer! do
    GenServer.call(__MODULE__, :reset_timer)
  end

  @doc """
  Manually trigger a db purge of expired tokens.
  Also resets the current timer.
  """
  def purge! do
    GenServer.call(__MODULE__, :sweep)
  end

  def init(state) do
    {:ok, reset_state_timer!(state)}
  end

  def handle_call(:reset_timer, _from, state) do
    {:reply, :ok, reset_state_timer!(state)}
  end

  def handle_call(:sweep, _from, state) do
    {:reply, :ok, sweep!(state)}
  end

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

  def handle_info(_, state) do
    {:ok, state}
  end

  defp sweep!(state) do
    GuardianDb.Token.purge_expired_tokens!
    reset_state_timer!(state)
  end

  defp reset_state_timer!(state) do
    if state[:timer] do
      Process.cancel_timer(state.timer)
    end

    timer = Process.send_after(self(), :sweep, interval())
    Map.merge(state, %{timer: timer})
  end

  defp interval do
    :guardian_db
    |> Application.get_env(GuardianDb)
    |> Keyword.get(:sweep_interval, 60)
    |> minute_to_ms
  end

  defp minute_to_ms(value) when is_binary(value) do
    value
    |> String.to_integer
    |> minute_to_ms
  end
  defp minute_to_ms(value) when value < 1, do: 1000
  defp minute_to_ms(value), do: round(value * 60 * 1000)
end

defmodule GuardianDb.ExpiredSweeper do
  @moduledoc """
  Periocially purges expired tokens from the DB.

  ## Example
    config :guardian_db, GuardianDb,
      sweep_interval: 60 # 1 hour

    # in your supervisor
      worker(GuardianDb.ExpiredSweeper, [])
  """
  use GenServer

  def start_link, do: start_link([])

  def start_link(state, _opts \\ []) do
    state = Enum.into(state, %{})

    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  Reset the purge timer.
  """
  def reset_timer! do
    GenServer.call(__MODULE__, :reset_timer)
  end

  @doc """
  Manually trigger a db purge of expired tokens.
  Also resets the current timer.
  """
  def purge! do
    GenServer.call(__MODULE__, :sweep)
  end

  def init(state) do
    {:ok, reset_state_timer!(state)}
  end

  def handle_call(:reset_timer, _from, state) do
    {:reply, :ok, reset_state_timer!(state)}
  end

  def handle_call(:sweep, _from, state) do
    {:reply, :ok, sweep!(state)}
  end

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

  def handle_info(_, state) do
    {:ok, state}
  end

  defp sweep!(state) do
    GuardianDb.Token.purge_expired_tokens!
    reset_state_timer!(state)
  end

  defp reset_state_timer!(state) do
    if state[:timer] do
      Process.cancel_timer(state.timer)
    end

    timer = Process.send_after(self(), :sweep, interval())
    Map.merge(state, %{timer: timer})
  end

  defp interval do
    :guardian_db
    |> Application.get_env(GuardianDb)
    |> Keyword.get(:sweep_interval, 60)
    |> minute_to_ms
  end

  defp minute_to_ms(value) when value < 1, do: 1000
  defp minute_to_ms(value), do: round(value * 60 * 1000)
end


add 'is_brand' to settings

add 'is_brand' to settings

defmodule Ello.Core.Network.User.Settings do
  use Ecto.Schema

  # A required field for all embedded documents, shouldn't be an interop issue
  @primary_key {:id, :binary_id, autogenerate: true}
  schema "" do
    field :is_public, :boolean, default: true
    field :is_hireable, :boolean, default: false
    field :is_collaborateable, :boolean, default: false
    field :is_brand, :boolean, default: false
    field :discoverable, :boolean, default: true

    # NSFW content (porn)
    field :posts_adult_content, :boolean, default: false
    field :views_adult_content, :boolean, default: false

    # Nudity content (artistic nudity)
    field :posts_nudity, :boolean, default: false

    # Enabled features
    field :has_commenting_enabled, :boolean, default: true
    field :has_loves_enabled, :boolean, default: true
    field :has_sharing_enabled, :boolean, default: true
    field :has_reposting_enabled, :boolean, default: true
    field :has_ad_notifications_enabled, :boolean, default: false
    field :has_auto_watch_enabled, :boolean, default: true
    field :has_announcements_enabled, :boolean, default: true

    # email notification
    field :notify_of_mentions_via_email, :boolean, default: true
    field :notify_of_new_followers_via_email, :boolean, default: true
    field :notify_of_comments_via_email, :boolean, default: true
    field :notify_of_comments_on_post_watch_via_email, :boolean, default: true
    field :notify_of_loves_via_email, :boolean, default: true
    field :notify_of_invitation_acceptances_via_email, :boolean, default: true
    field :notify_of_reposts_via_email, :boolean, default: true
    field :notify_of_watches_via_email, :boolean, default: true

    # push notifications
    field :notify_of_mentions_via_push, :boolean, default: true
    field :notify_of_new_followers_via_push, :boolean, default: true
    field :notify_of_comments_via_push, :boolean, default: true
    field :notify_of_comments_on_post_watch_via_push, :boolean, default: true
    field :notify_of_loves_via_push, :boolean, default: true
    field :notify_of_invitation_acceptances_via_push, :boolean, default: true
    field :notify_of_reposts_via_push, :boolean, default: true
    field :notify_of_watches_via_push, :boolean, default: true
    field :notify_of_announcements_via_push, :boolean, default: true
    field :notify_of_approved_submissions_from_following_via_email, :boolean, default: true
    field :notify_of_approved_submissions_via_push, :boolean, default: true
    field :notify_of_featured_category_post_via_email, :boolean, default: true
    field :notify_of_featured_category_post_via_push, :boolean, default: true
    field :notify_of_approved_submissions_from_following_via_push, :boolean, default: true
    field :notify_of_what_you_missed_via_email, :boolean, default: true

    # misc
    field :allows_analytics, :boolean, default: true

    # Product Updates
    field :subscribe_to_users_email_list, :boolean, default: true

    # Knowtify Tops & Tricks
    field :subscribe_to_onboarding_drip, :boolean, default: true

    # Newsletters
    field :subscribe_to_weekly_ello, :boolean, default: true
    field :subscribe_to_daily_ello, :boolean, default: true
  end
end

defmodule Ello.Core.Network.User.Settings do
  use Ecto.Schema

  # A required field for all embedded documents, shouldn't be an interop issue
  @primary_key {:id, :binary_id, autogenerate: true}
  schema "" do
    field :is_public, :boolean, default: true
    field :is_hireable, :boolean, default: false
    field :is_collaborateable, :boolean, default: false
    field :discoverable, :boolean, default: true

    # NSFW content (porn)
    field :posts_adult_content, :boolean, default: false
    field :views_adult_content, :boolean, default: false

    # Nudity content (artistic nudity)
    field :posts_nudity, :boolean, default: false

    # Enabled features
    field :has_commenting_enabled, :boolean, default: true
    field :has_loves_enabled, :boolean, default: true
    field :has_sharing_enabled, :boolean, default: true
    field :has_reposting_enabled, :boolean, default: true
    field :has_ad_notifications_enabled, :boolean, default: false
    field :has_auto_watch_enabled, :boolean, default: true
    field :has_announcements_enabled, :boolean, default: true

    # email notification
    field :notify_of_mentions_via_email, :boolean, default: true
    field :notify_of_new_followers_via_email, :boolean, default: true
    field :notify_of_comments_via_email, :boolean, default: true
    field :notify_of_comments_on_post_watch_via_email, :boolean, default: true
    field :notify_of_loves_via_email, :boolean, default: true
    field :notify_of_invitation_acceptances_via_email, :boolean, default: true
    field :notify_of_reposts_via_email, :boolean, default: true
    field :notify_of_watches_via_email, :boolean, default: true

    # push notifications
    field :notify_of_mentions_via_push, :boolean, default: true
    field :notify_of_new_followers_via_push, :boolean, default: true
    field :notify_of_comments_via_push, :boolean, default: true
    field :notify_of_comments_on_post_watch_via_push, :boolean, default: true
    field :notify_of_loves_via_push, :boolean, default: true
    field :notify_of_invitation_acceptances_via_push, :boolean, default: true
    field :notify_of_reposts_via_push, :boolean, default: true
    field :notify_of_watches_via_push, :boolean, default: true
    field :notify_of_announcements_via_push, :boolean, default: true
    field :notify_of_approved_submissions_from_following_via_email, :boolean, default: true
    field :notify_of_approved_submissions_via_push, :boolean, default: true
    field :notify_of_featured_category_post_via_email, :boolean, default: true
    field :notify_of_featured_category_post_via_push, :boolean, default: true
    field :notify_of_approved_submissions_from_following_via_push, :boolean, default: true
    field :notify_of_what_you_missed_via_email, :boolean, default: true

    # misc
    field :allows_analytics, :boolean, default: true

    # Product Updates
    field :subscribe_to_users_email_list, :boolean, default: true

    # Knowtify Tops & Tricks
    field :subscribe_to_onboarding_drip, :boolean, default: true

    # Newsletters
    field :subscribe_to_weekly_ello, :boolean, default: true
    field :subscribe_to_daily_ello, :boolean, default: true
  end
end


Ensure consolidator works when functions are reordered

defmodule Protocol.Consolidation do
  @moduledoc """
  Module responsible for consolidating protocols and helpers for
  extracting protocols and implementations from code paths for
  consolidation.
  """

  @doc """
  Extract all protocols from the given paths.

  The paths can be either a char list or a string. Internally
  they are worked on as char lists, so passing them as lists
  avoid extra conversion.

  ## Examples

      # Get Elixir's ebin and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.Consolidation.extract_protocols([path])
      iex> Enumerable in mods
      true

  """
  @spec extract_protocols([char_list | String.t]) :: [atom]
  def extract_protocols(paths) do
    extract_matching_by_attribute paths, 'Elixir.',
      fn module, attributes ->
        case attributes[:protocol] do
          [fallback_to_any: _, consolidated: _] -> module
          _ -> nil
        end
      end
  end

  @doc """
  Extract all types implemented for the given protocol from
  the given paths.

  The paths can be either a char list or a string. Internally
  they are worked on as char lists, so passing them as lists
  avoid extra conversion.

  ## Examples

      # Get Elixir's ebin and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.Consolidation.extract_impls(Enumerable, [path])
      iex> List in mods
      true

  """
  @spec extract_impls(module, [char_list | String.t]) :: [atom]
  def extract_impls(protocol, paths) when is_atom(protocol) do
    prefix = atom_to_list(protocol) ++ '.'
    extract_matching_by_attribute paths, prefix, fn
      _mod, attributes ->
        case attributes[:impl] do
          [protocol: ^protocol, for: for] -> for
          _ -> nil
        end
    end
  end

  defp extract_matching_by_attribute(paths, prefix, callback) do
    lc path inlist paths,
       file inlist list_dir(path),
       mod = extract_from_file(path, file, prefix, callback),
       do: mod
  end

  defp list_dir(path) when is_list(path) do
    case :file.list_dir(path) do
      { :ok, files } -> files
      _ -> []
    end
  end

  defp list_dir(path), do: list_dir(to_char_list(path))

  defp extract_from_file(path, file, prefix, callback) do
    if :lists.prefix(prefix, file) and Path.extname(file) == '.beam' do
      extract_from_beam(Path.join(path, file), callback)
    end
  end

  defp extract_from_beam(file, callback) do
    case :beam_lib.chunks(file, [:attributes]) do
      {:ok, { module, [attributes: attributes] } } ->
        callback.(module, attributes)
       _ ->
         nil
    end
  end

  @doc """
  Receives a protocol and a list of implementations and
  consolidates the given protocol. Consolidation happens
  by changing the protocol `impl_for` in the abstract
  format to have fast lookup rules.

  It returns the updated version of the protocol bytecode.
  A given bytecode or protocol implementation can be checked
  to be consolidated or not by analyzing the protocol
  attribute:

      Enumerable.__info__(:attributes)[:protocol]

  If the first element of the tuple is true, it means
  the protocol was consolidated.

  This function does not load the protocol at any point
  nor loads the new bytecode for the compiled module.
  """
  @spec apply_to(module, [module]) ::
    { :ok, binary } |
    { :error, :not_a_protocol } |
    { :error, :no_beam_info }

  def apply_to(protocol, types) when is_atom(protocol) do
    { :ok, protocol }
    |> ensure_protocol
    |> change_debug_info(types)
    |> compile
  end

  # Ensure the given module is loaded and is a protocol.
  defp ensure_protocol({ :ok, protocol }) do
    case :beam_lib.chunks(beam_file(protocol), [:abstract_code, :attributes]) do
      { :ok, { ^protocol, [abstract_code: { _raw, abstract_code },
                           attributes: attributes] } } ->
        case attributes[:protocol] do
          [fallback_to_any: any, consolidated: _] ->
            { :ok, protocol, any, abstract_code }
          _ ->
            { :error, :not_a_protocol }
        end
      _ ->
        { :error, :no_beam_info }
    end
  end

  defp ensure_protocol(other), do: other

  defp beam_file(module) when is_atom(module) do
    case :code.which(module) do
      :non_existing -> module
      file -> file
    end
  end

  # Change the debug information to the optimized
  # impl_for/1 dispatch version.
  defp change_debug_info({ :ok, protocol, any, code }, types) do
    types   = if any, do: types, else: List.delete(types, Any)
    records = types -- Protocol.builtin
    builtin = Protocol.builtin -- (Protocol.builtin -- types)
    builtin = if records != [], do: [Record|builtin], else: builtin
    change_impl_for(code, protocol, builtin, records, false, [])
  end

  defp change_debug_info(other, _types), do: other

  defp change_impl_for([{ :attribute, line, :protocol, _ }|t], protocol, builtin, records, _, acc) do
    attr = [fallback_to_any: Any in builtin, consolidated: true]
    change_impl_for(t, protocol, builtin, records, true,
                    [{ :attribute, line, :protocol, attr }|acc])
  end

  defp change_impl_for([{ :function, line, :impl_for, 1, _ }|t], protocol, builtin, records, is_protocol, acc) do
    clauses = lc type inlist builtin, do: clause_for(type, protocol, line)

    unless Any in builtin do
      clauses = clauses ++ [fallback_clause_for(nil, protocol, line)]
    end

    change_impl_for(t, protocol, builtin, records, is_protocol,
                    [{ :function, line, :impl_for, 1, clauses }|acc])
  end

  defp change_impl_for([{ :function, line, :rec_impl_for, 1, _ }|t], protocol, builtin, records, is_protocol, acc) do
    fallback = if Tuple in builtin, do: Module.concat(protocol, Tuple)
    clauses  = lc type inlist records, do: record_clause_for(type, protocol, line)
    clauses  = clauses ++ [fallback_clause_for(fallback, protocol, line)]

    change_impl_for(t, protocol, builtin, records, is_protocol,
                    [{ :function, line, :rec_impl_for, 1, clauses }|acc])
  end

  defp change_impl_for([h|t], protocol, info, types, is_protocol, acc) do
    change_impl_for(t, protocol, info, types, is_protocol, [h|acc])
  end

  defp change_impl_for([], protocol, _info, _types, is_protocol, acc) do
    if is_protocol do
      { :ok, protocol, Enum.reverse(acc) }
    else
      { :error, :not_a_protocol }
    end
  end

  defp clause_for(Tuple, protocol, line),     do: builtin_clause_for(Tuple, :is_tuple, protocol, line)
  defp clause_for(Atom, protocol, line),      do: builtin_clause_for(Atom, :is_atom, protocol, line)
  defp clause_for(List, protocol, line),      do: builtin_clause_for(List, :is_list, protocol, line)
  defp clause_for(BitString, protocol, line), do: builtin_clause_for(BitString, :is_bitstring, protocol, line)
  defp clause_for(Integer, protocol, line),   do: builtin_clause_for(Integer, :is_integer, protocol, line)
  defp clause_for(Float, protocol, line),     do: builtin_clause_for(Float, :is_float, protocol, line)
  defp clause_for(Function, protocol, line),  do: builtin_clause_for(Function, :is_function, protocol, line)
  defp clause_for(PID, protocol, line),       do: builtin_clause_for(PID, :is_pid, protocol, line)
  defp clause_for(Port, protocol, line),      do: builtin_clause_for(Port, :is_port, protocol, line)
  defp clause_for(Reference, protocol, line), do: builtin_clause_for(Reference, :is_reference, protocol, line)

  defp clause_for(Any, protocol, line) do
    {:clause, line, [{:var, line, :_}], [],
      [{ :atom, line, Module.concat(protocol, Any) }]}
  end

  defp clause_for(Record, _protocol, line) do
    {:clause, line, [{:var, line, :x}],
      [[{:op, line, :andalso,
          {:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :is_tuple}},
            [{:var, line, :x}]},
          {:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
            [{:call, line,
              {:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
              [{:integer, line, 1}, {:var, line, :x}]
            }]},
      }]],
      [{:call, line,
          {:atom, line, :rec_impl_for},
          [{:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
            [{:integer, line, 1}, {:var, line, :x}]}]}]}
  end

  defp builtin_clause_for(mod, guard, protocol, line) do
    {:clause, line,
      [{:var, line, :x}],
      [[{:call, line,
          {:remote, line, {:atom, line, :erlang}, {:atom, line, guard}},
          [{:var, line, :x}],
      }]],
      [{:atom, line, Module.concat(protocol, mod)}]}
  end

  defp record_clause_for(other, protocol, line) do
    {:clause, line, [{:atom, line, other}], [],
      [{:atom, line, Module.concat(protocol, other)}]}
  end

  defp fallback_clause_for(value, _protocol, line) do
    {:clause, line, [{:var, line, :_}], [],
      [{ :atom, line, value }]}
  end

  # Finally compile the module and emit its bytecode.
  defp compile({ :ok, protocol, code }) do
    opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
    { :ok, ^protocol, binary, _warnings } = :compile.forms(code, [:return|opts])
    { :ok, binary }
  end

  defp compile(other) do
    other
  end
end

defmodule Protocol.Consolidation do
  @moduledoc """
  Module responsible for consolidating protocols and helpers for
  extracting protocols and implementations from code paths for
  consolidation.
  """

  @doc """
  Extract all protocols from the given paths.

  The paths can be either a char list or a string. Internally
  they are worked on as char lists, so passing them as lists
  avoid extra conversion.

  ## Examples

      # Get Elixir's ebin and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.Consolidation.extract_protocols([path])
      iex> Enumerable in mods
      true

  """
  @spec extract_protocols([char_list | String.t]) :: [atom]
  def extract_protocols(paths) do
    extract_matching_by_attribute paths, 'Elixir.',
      fn module, attributes ->
        case attributes[:protocol] do
          [fallback_to_any: _, consolidated: _] -> module
          _ -> nil
        end
      end
  end

  @doc """
  Extract all types implemented for the given protocol from
  the given paths.

  The paths can be either a char list or a string. Internally
  they are worked on as char lists, so passing them as lists
  avoid extra conversion.

  ## Examples

      # Get Elixir's ebin and retrieve all protocols
      iex> path = :code.lib_dir(:elixir, :ebin)
      iex> mods = Protocol.Consolidation.extract_impls(Enumerable, [path])
      iex> List in mods
      true

  """
  @spec extract_impls(module, [char_list | String.t]) :: [atom]
  def extract_impls(protocol, paths) when is_atom(protocol) do
    prefix = atom_to_list(protocol) ++ '.'
    extract_matching_by_attribute paths, prefix, fn
      _mod, attributes ->
        case attributes[:impl] do
          [protocol: ^protocol, for: for] -> for
          _ -> nil
        end
    end
  end

  defp extract_matching_by_attribute(paths, prefix, callback) do
    lc path inlist paths,
       file inlist list_dir(path),
       mod = extract_from_file(path, file, prefix, callback),
       do: mod
  end

  defp list_dir(path) when is_list(path) do
    case :file.list_dir(path) do
      { :ok, files } -> files
      _ -> []
    end
  end

  defp list_dir(path), do: list_dir(to_char_list(path))

  defp extract_from_file(path, file, prefix, callback) do
    if :lists.prefix(prefix, file) and Path.extname(file) == '.beam' do
      extract_from_beam(Path.join(path, file), callback)
    end
  end

  defp extract_from_beam(file, callback) do
    case :beam_lib.chunks(file, [:attributes]) do
      {:ok, { module, [attributes: attributes] } } ->
        callback.(module, attributes)
       _ ->
         nil
    end
  end

  @doc """
  Receives a protocol and a list of implementations and
  consolidates the given protocol. Consolidation happens
  by changing the protocol `impl_for` in the abstract
  format to have fast lookup rules.

  It returns the updated version of the protocol bytecode.
  A given bytecode or protocol implementation can be checked
  to be consolidated or not by analyzing the protocol
  attribute:

      Enumerable.__info__(:attributes)[:protocol]

  If the first element of the tuple is true, it means
  the protocol was consolidated.

  This function does not load the protocol at any point
  nor loads the new bytecode for the compiled module.
  """
  @spec apply_to(module, [module]) ::
    { :ok, binary } |
    { :error, :not_a_protocol } |
    { :error, :no_beam_info }

  def apply_to(protocol, types) when is_atom(protocol) do
    { :ok, protocol }
    |> ensure_protocol
    |> change_debug_info(types)
    |> compile
  end

  # Ensure the given module is loaded and is a protocol.
  defp ensure_protocol({ :ok, protocol }) do
    case :beam_lib.chunks(beam_file(protocol), [:abstract_code, :attributes]) do
      { :ok, { ^protocol, [abstract_code: { _raw, abstract_code },
                           attributes: attributes] } } ->
        case attributes[:protocol] do
          [fallback_to_any: any, consolidated: _] ->
            { :ok, protocol, any, abstract_code }
          _ ->
            { :error, :not_a_protocol }
        end
      _ ->
        { :error, :no_beam_info }
    end
  end

  defp ensure_protocol(other), do: other

  defp beam_file(module) when is_atom(module) do
    case :code.which(module) do
      :non_existing -> module
      file -> file
    end
  end

  # Change the debug information to the optimized
  # impl_for/1 dispatch version.
  defp change_debug_info({ :ok, protocol, any, code }, types) do
    types   = if any, do: types, else: List.delete(types, Any)
    records = types -- Protocol.builtin
    builtin = Protocol.builtin -- (Protocol.builtin -- types)
    builtin = if records != [], do: [Record|builtin], else: builtin
    change_impl_for(code, protocol, builtin, records, [])
  end

  defp change_debug_info(other, _types), do: other

  defp change_impl_for([{ :attribute, line, :protocol, _ }|t], protocol, builtin, records, acc) do
    attr = [fallback_to_any: Any in builtin, consolidated: true]
    change_impl_for(t, protocol, builtin, records,
                    [{ :attribute, line, :protocol, attr }|acc])
  end

  defp change_impl_for([{ :function, line, :impl_for, 1, _ }|t], protocol, builtin, records, acc) do
    clauses = lc type inlist builtin, do: clause_for(type, protocol, line)

    unless Any in builtin do
      clauses = clauses ++ [fallback_clause_for(nil, protocol, line)]
    end

    change_impl_for(t, protocol, builtin, records,
                    [{ :function, line, :impl_for, 1, clauses }|acc])
  end

  defp change_impl_for([{ :function, line, :rec_impl_for, 1, _ }|t], protocol, builtin, records, acc) do
    fallback = if Tuple in builtin, do: Module.concat(protocol, Tuple)
    clauses  = lc type inlist records, do: record_clause_for(type, protocol, line)
    clauses  = clauses ++ [fallback_clause_for(fallback, protocol, line)]

    { :ok, protocol,
      Enum.reverse(acc) ++ [{ :function, line, :rec_impl_for, 1, clauses }|t] }
  end

  defp change_impl_for([h|t], protocol, info, types, acc) do
    change_impl_for(t, protocol, info, types, [h|acc])
  end

  defp change_impl_for([], _protocol, _info, _types, _acc) do
    { :error, :not_a_protocol }
  end

  defp clause_for(Tuple, protocol, line),     do: builtin_clause_for(Tuple, :is_tuple, protocol, line)
  defp clause_for(Atom, protocol, line),      do: builtin_clause_for(Atom, :is_atom, protocol, line)
  defp clause_for(List, protocol, line),      do: builtin_clause_for(List, :is_list, protocol, line)
  defp clause_for(BitString, protocol, line), do: builtin_clause_for(BitString, :is_bitstring, protocol, line)
  defp clause_for(Integer, protocol, line),   do: builtin_clause_for(Integer, :is_integer, protocol, line)
  defp clause_for(Float, protocol, line),     do: builtin_clause_for(Float, :is_float, protocol, line)
  defp clause_for(Function, protocol, line),  do: builtin_clause_for(Function, :is_function, protocol, line)
  defp clause_for(PID, protocol, line),       do: builtin_clause_for(PID, :is_pid, protocol, line)
  defp clause_for(Port, protocol, line),      do: builtin_clause_for(Port, :is_port, protocol, line)
  defp clause_for(Reference, protocol, line), do: builtin_clause_for(Reference, :is_reference, protocol, line)

  defp clause_for(Any, protocol, line) do
    {:clause, line, [{:var, line, :_}], [],
      [{ :atom, line, Module.concat(protocol, Any) }]}
  end

  defp clause_for(Record, _protocol, line) do
    {:clause, line, [{:var, line, :x}],
      [[{:op, line, :andalso,
          {:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :is_tuple}},
            [{:var, line, :x}]},
          {:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :is_atom}},
            [{:call, line,
              {:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
              [{:integer, line, 1}, {:var, line, :x}]
            }]},
      }]],
      [{:call, line,
          {:atom, line, :rec_impl_for},
          [{:call, line,
            {:remote, line, {:atom, line, :erlang}, {:atom, line, :element}},
            [{:integer, line, 1}, {:var, line, :x}]}]}]}
  end

  defp builtin_clause_for(mod, guard, protocol, line) do
    {:clause, line,
      [{:var, line, :x}],
      [[{:call, line,
          {:remote, line, {:atom, line, :erlang}, {:atom, line, guard}},
          [{:var, line, :x}],
      }]],
      [{:atom, line, Module.concat(protocol, mod)}]}
  end

  defp record_clause_for(other, protocol, line) do
    {:clause, line, [{:atom, line, other}], [],
      [{:atom, line, Module.concat(protocol, other)}]}
  end

  defp fallback_clause_for(value, _protocol, line) do
    {:clause, line, [{:var, line, :_}], [],
      [{ :atom, line, value }]}
  end

  # Finally compile the module and emit its bytecode.
  defp compile({ :ok, protocol, code }) do
    opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
    { :ok, ^protocol, binary, _warnings } = :compile.forms(code, [:return|opts])
    { :ok, binary }
  end

  defp compile(other) do
    other
  end
end


Use one-liner form

Use one-liner form

defmodule Worker.Server do
  use GenServer
  require Logger
  alias Worker.IndexingSupervisor
  alias Worker.Indexer

  @doc """
  Triggers the indexing of projects.
  Returns :ok, or :rejected, in case indexing for any of the given projects is already running.
  """
  def index_projects(projects) do
    GenServer.call(__MODULE__, {:index, projects})
  end

  ##########################################################

  def start_link(opts) do
    Logger.debug "Start #{__MODULE__}"
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok), do: {:ok, %{}}

  @impl true
  def handle_call({:index, projects}, _from, refs) do

    conflicts = MapSet.intersection(
      MapSet.new(projects),
      MapSet.new(Map.keys(refs)))
      |> MapSet.to_list

    if Enum.count(conflicts) > 0 do
      {
        :reply,
        {
          :rejected, "Other indexing processes still running. "
          <> "Conflicts: #{Enum.join(Enum.map(conflicts, fn conflict -> "'" <> conflict <> "'" end), ", ")}"
        },
        refs
      }
    else
      new_refs = start_reindex_processes projects
      {
        :reply,
        {:ok, "Start indexing #{Enum.join(projects, ", ")}"},
        Map.merge(refs, new_refs)
      }
    end
  end

  @impl true
  def handle_info({_, {:finished_reindex_project, project}}, refs) do
    {:noreply, Map.delete(refs, project)}
  end
  def handle_info({:DOWN, _ref, :process, _pid, :normal}, refs), do: {:noreply, refs}
  def handle_info({:DOWN, ref, :process, _pid, _error}, refs) do
    # TODO maybe prevent error from being logged automatically. Instead log _error here.
    {project, _ref} = Enum.find(Map.to_list(refs), fn {_,v} -> v == ref end) # TODO Handle not found
    Logger.error "Something went wrong. Could not finish reindexing '#{project}'"
    {:noreply, Map.delete(refs, project)}
  end
  def handle_info(msg, state) do
    Logger.error "Something went wrong #{msg}"
    {:noreply, state}
  end

  defp start_reindex_processes(projects) do
    for project <- projects, into: %{} do
      task = Task.Supervisor.async_nolink(
        IndexingSupervisor, Indexer, :reindex, [project])
        {
          project,
          task.ref
        }
    end
  end
end

defmodule Worker.Server do
  use GenServer
  require Logger
  alias Worker.IndexingSupervisor
  alias Worker.Indexer

  @doc """
  Triggers the indexing of projects.
  Returns :ok, or :rejected, in case indexing for any of the given projects is already running.
  """
  def index_projects(projects) do
    GenServer.call(__MODULE__, {:index, projects})
  end

  ##########################################################

  def start_link(opts) do
    Logger.debug "Start #{__MODULE__}"
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  @impl true
  def init(:ok) do
    {:ok, %{}}
  end

  @impl true
  def handle_call({:index, projects}, _from, refs) do

    conflicts = MapSet.intersection(
      MapSet.new(projects),
      MapSet.new(Map.keys(refs)))
      |> MapSet.to_list

    if Enum.count(conflicts) > 0 do
      {
        :reply,
        {
          :rejected, "Other indexing processes still running. "
          <> "Conflicts: #{Enum.join(Enum.map(conflicts, fn conflict -> "'" <> conflict <> "'" end), ", ")}"
        },
        refs
      }
    else
      new_refs = start_reindex_processes projects
      {
        :reply,
        {:ok, "Start indexing #{Enum.join(projects, ", ")}"},
        Map.merge(refs, new_refs)
      }
    end
  end

  @impl true
  def handle_info({_, {:finished_reindex_project, project}}, refs) do
    {:noreply, Map.delete(refs, project)}
  end
  def handle_info({:DOWN, _ref, :process, _pid, :normal}, refs), do: {:noreply, refs}
  def handle_info({:DOWN, ref, :process, _pid, _error}, refs) do
    # TODO maybe prevent error from being logged automatically. Instead log _error here.
    {project, _ref} = Enum.find(Map.to_list(refs), fn {_,v} -> v == ref end) # TODO Handle not found
    Logger.error "Something went wrong. Could not finish reindexing '#{project}'"
    {:noreply, Map.delete(refs, project)}
  end
  def handle_info(msg, state) do
    Logger.error "Something went wrong #{msg}"
    {:noreply, state}
  end

  defp start_reindex_processes(projects) do
    for project <- projects, into: %{} do
      task = Task.Supervisor.async_nolink(
        IndexingSupervisor, Indexer, :reindex, [project])
        {
          project,
          task.ref
        }
    end
  end
end


Behaviour updates

Behaviour updates

defmodule FarmbotFirmware.SideEffects do
  @moduledoc """
  Callback functions that the firmware process will call on certain events.
  """

  alias FarmbotFirmware.{GCODE, Param}

  @type axis :: :x | :y | :z

  @doc "While in state `:boot`, the firmware needs to load its params."
  @callback load_params :: [{Param.t(), float() | nil}]

  @callback handle_position(x: float(), y: float(), z: float()) :: any()
  @callback handle_load(x: float(), y: float(), z: float()) :: any()
  @callback handle_position_change([{axis(), float()}]) :: any()
  @callback handle_encoders_scaled(x: float(), y: float(), z: float()) :: any()
  @callback handle_encoders_raw(x: float(), y: float(), z: float()) :: any()
  @callback handle_parameter_value([{Param.t(), float()}]) :: any()
  @callback handle_parameter_calibration_value([{Param.t(), float()}]) :: any()
  @callback handle_end_stops(
              xa: 0 | 1,
              xb: 0 | 1,
              ya: 0 | 1,
              yb: 0 | 1,
              za: 0 | 1,
              zb: 0 | 1
            ) ::
              any()
  @callback handle_emergency_lock() :: any()
  @callback handle_emergency_unlock() :: any()
  @callback handle_pin_value(p: integer(), v: integer()) :: any()
  @callback handle_software_version([String.t()]) :: any()
  @callback handle_busy(boolean()) :: any()
  @callback handle_idle(boolean()) :: any()

  @type axis_state ::
          :stop | :idle | :begin | :crawl | :decelerate | :accelerate
  @callback handle_axis_state([{axis(), axis_state}]) :: any()

  @callback handle_axis_timeout(axis()) :: any()

  @type calibration_state :: :idle | :home | :end
  @callback handle_calibration_state([{axis(), calibration_state()}]) :: any()

  @callback handle_home_complete([axis()]) :: any()

  @callback handle_input_gcode(GCODE.t()) :: any()
  @callback handle_output_gcode(GCODE.t()) :: any()
  @callback handle_debug_message([String.t()]) :: any()
  @callback handle_calibration_error() :: any()
  @callback handle_invalid_command() :: any()
  @callback handle_no_configuration() :: any()
  @callback handle_stall_detected() :: any()
  @callback handle_unknown_error(any()) :: any()
end

defmodule FarmbotFirmware.SideEffects do
  @moduledoc """
  Callback functions that the firmware process will call on certain events.
  """

  alias FarmbotFirmware.{GCODE, Param}

  @type axis :: :x | :y | :z

  @doc "While in state `:boot`, the firmware needs to load its params."
  @callback load_params :: [{Param.t(), float() | nil}]

  @callback handle_position(x: float(), y: float(), z: float()) :: any()
  @callback handle_load(x: float(), y: float(), z: float()) :: any()
  @callback handle_position_change([{axis(), float()}]) :: any()
  @callback handle_encoders_scaled(x: float(), y: float(), z: float()) :: any()
  @callback handle_encoders_raw(x: float(), y: float(), z: float()) :: any()
  @callback handle_parameter_value([{Param.t(), float()}]) :: any()
  @callback handle_parameter_calibration_value([{Param.t(), float()}]) :: any()
  @callback handle_end_stops(
              xa: 0 | 1,
              xb: 0 | 1,
              ya: 0 | 1,
              yb: 0 | 1,
              za: 0 | 1,
              zb: 0 | 1
            ) ::
              any()
  @callback handle_emergency_lock() :: any()
  @callback handle_emergency_unlock() :: any()
  @callback handle_pin_value(p: integer(), v: integer()) :: any()
  @callback handle_software_version([String.t()]) :: any()
  @callback handle_busy(boolean()) :: any()
  @callback handle_idle(boolean()) :: any()

  @type axis_state ::
          :stop | :idle | :begin | :crawl | :decelerate | :accelerate
  @callback handle_axis_state([{axis(), axis_state}]) :: any()

  @callback handle_axis_timeout(axis()) :: any()

  @type calibration_state :: :idle | :home | :end
  @callback handle_calibration_state([{axis(), calibration_state()}]) :: any()

  @callback handle_home_complete([axis()]) :: any()

  @callback handle_input_gcode(GCODE.t()) :: any()
  @callback handle_output_gcode(GCODE.t()) :: any()
  @callback handle_debug_message([String.t()]) :: any()
end


Add test to List.ascii_printable?/2 (#8655)

Add test that checks improper lists with limit

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

defmodule ListTest do
  use ExUnit.Case, async: true

  doctest List

  test "cons cell precedence" do
    assert [1 | List.flatten([2, 3])] == [1, 2, 3]
  end

  test "optional comma" do
    assert Code.eval_string("[1,]") == {[1], []}
    assert Code.eval_string("[1, 2, 3,]") == {[1, 2, 3], []}
  end

  test "partial application" do
    assert (&[&1, 2]).(1) == [1, 2]
    assert (&[&1, &2]).(1, 2) == [1, 2]
    assert (&[&2, &1]).(2, 1) == [1, 2]
    assert (&[&1 | &2]).(1, 2) == [1 | 2]
    assert (&[&1, &2 | &3]).(1, 2, 3) == [1, 2 | 3]
  end

  test "wrap/1" do
    assert List.wrap([1, 2, 3]) == [1, 2, 3]
    assert List.wrap(1) == [1]
    assert List.wrap(nil) == []
  end

  test "flatten/1" do
    assert List.flatten([1, 2, 3]) == [1, 2, 3]
    assert List.flatten([1, [2], 3]) == [1, 2, 3]
    assert List.flatten([[1, [2], 3]]) == [1, 2, 3]

    assert List.flatten([]) == []
    assert List.flatten([[]]) == []
  end

  test "flatten/2" do
    assert List.flatten([1, 2, 3], [4, 5]) == [1, 2, 3, 4, 5]
    assert List.flatten([1, [2], 3], [4, 5]) == [1, 2, 3, 4, 5]
    assert List.flatten([[1, [2], 3]], [4, 5]) == [1, 2, 3, 4, 5]
  end

  test "foldl/3" do
    assert List.foldl([1, 2, 3], 0, fn x, y -> x + y end) == 6
    assert List.foldl([1, 2, 3], 10, fn x, y -> x + y end) == 16
    assert List.foldl([1, 2, 3, 4], 0, fn x, y -> x - y end) == 2
  end

  test "foldr/3" do
    assert List.foldr([1, 2, 3], 0, fn x, y -> x + y end) == 6
    assert List.foldr([1, 2, 3], 10, fn x, y -> x + y end) == 16
    assert List.foldr([1, 2, 3, 4], 0, fn x, y -> x - y end) == -2
  end

  test "duplicate/2" do
    assert List.duplicate(1, 3) == [1, 1, 1]
    assert List.duplicate([1], 1) == [[1]]
  end

  test "last/1" do
    assert List.last([]) == nil
    assert List.last([1]) == 1
    assert List.last([1, 2, 3]) == 3
  end

  test "zip/1" do
    assert List.zip([[1, 4], [2, 5], [3, 6]]) == [{1, 2, 3}, {4, 5, 6}]
    assert List.zip([[1, 4], [2, 5, 0], [3, 6]]) == [{1, 2, 3}, {4, 5, 6}]
    assert List.zip([[1], [2, 5], [3, 6]]) == [{1, 2, 3}]
    assert List.zip([[1, 4], [2, 5], []]) == []
  end

  test "keyfind/4" do
    assert List.keyfind([a: 1, b: 2], :a, 0) == {:a, 1}
    assert List.keyfind([a: 1, b: 2], 2, 1) == {:b, 2}
    assert List.keyfind([a: 1, b: 2], :c, 0) == nil
  end

  test "keyreplace/4" do
    assert List.keyreplace([a: 1, b: 2], :a, 0, {:a, 3}) == [a: 3, b: 2]
    assert List.keyreplace([a: 1], :b, 0, {:b, 2}) == [a: 1]
  end

  test "keysort/2" do
    assert List.keysort([a: 4, b: 3, c: 5], 1) == [b: 3, a: 4, c: 5]
    assert List.keysort([a: 4, c: 1, b: 2], 0) == [a: 4, b: 2, c: 1]
  end

  test "keystore/4" do
    assert List.keystore([a: 1, b: 2], :a, 0, {:a, 3}) == [a: 3, b: 2]
    assert List.keystore([a: 1], :b, 0, {:b, 2}) == [a: 1, b: 2]
  end

  test "keymember?/3" do
    assert List.keymember?([a: 1, b: 2], :a, 0) == true
    assert List.keymember?([a: 1, b: 2], 2, 1) == true
    assert List.keymember?([a: 1, b: 2], :c, 0) == false
  end

  test "keydelete/3" do
    assert List.keydelete([a: 1, b: 2], :a, 0) == [{:b, 2}]
    assert List.keydelete([a: 1, b: 2], 2, 1) == [{:a, 1}]
    assert List.keydelete([a: 1, b: 2], :c, 0) == [{:a, 1}, {:b, 2}]
  end

  test "insert_at/3" do
    assert List.insert_at([1, 2, 3], 0, 0) == [0, 1, 2, 3]
    assert List.insert_at([1, 2, 3], 3, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], 2, 0) == [1, 2, 0, 3]
    assert List.insert_at([1, 2, 3], 10, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], -1, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], -4, 0) == [0, 1, 2, 3]
    assert List.insert_at([1, 2, 3], -10, 0) == [0, 1, 2, 3]
  end

  test "replace_at/3" do
    assert List.replace_at([1, 2, 3], 0, 0) == [0, 2, 3]
    assert List.replace_at([1, 2, 3], 1, 0) == [1, 0, 3]
    assert List.replace_at([1, 2, 3], 2, 0) == [1, 2, 0]
    assert List.replace_at([1, 2, 3], 3, 0) == [1, 2, 3]
    assert List.replace_at([1, 2, 3], -1, 0) == [1, 2, 0]
    assert List.replace_at([1, 2, 3], -4, 0) == [1, 2, 3]
  end

  test "update_at/3" do
    assert List.update_at([1, 2, 3], 0, &(&1 + 1)) == [2, 2, 3]
    assert List.update_at([1, 2, 3], 1, &(&1 + 1)) == [1, 3, 3]
    assert List.update_at([1, 2, 3], 2, &(&1 + 1)) == [1, 2, 4]
    assert List.update_at([1, 2, 3], 3, &(&1 + 1)) == [1, 2, 3]
    assert List.update_at([1, 2, 3], -1, &(&1 + 1)) == [1, 2, 4]
    assert List.update_at([1, 2, 3], -4, &(&1 + 1)) == [1, 2, 3]
  end

  test "delete_at/2" do
    for index <- [-1, 0, 1] do
      assert List.delete_at([], index) == []
    end

    assert List.delete_at([1, 2, 3], 0) == [2, 3]
    assert List.delete_at([1, 2, 3], 2) == [1, 2]
    assert List.delete_at([1, 2, 3], 3) == [1, 2, 3]
    assert List.delete_at([1, 2, 3], -1) == [1, 2]
    assert List.delete_at([1, 2, 3], -3) == [2, 3]
    assert List.delete_at([1, 2, 3], -4) == [1, 2, 3]
  end

  test "pop_at/3" do
    for index <- [-1, 0, 1] do
      assert List.pop_at([], index) == {nil, []}
    end

    assert List.pop_at([1], 1, 2) == {2, [1]}
    assert List.pop_at([1, 2, 3], 0) == {1, [2, 3]}
    assert List.pop_at([1, 2, 3], 2) == {3, [1, 2]}
    assert List.pop_at([1, 2, 3], 3) == {nil, [1, 2, 3]}
    assert List.pop_at([1, 2, 3], -1) == {3, [1, 2]}
    assert List.pop_at([1, 2, 3], -3) == {1, [2, 3]}
    assert List.pop_at([1, 2, 3], -4) == {nil, [1, 2, 3]}
  end

  describe "starts_with?/2" do
    test "list and prefix are equal" do
      assert List.starts_with?([], [])
      assert List.starts_with?([1], [1])
      assert List.starts_with?([1, 2, 3], [1, 2, 3])
    end

    test "proper lists" do
      refute List.starts_with?([1], [1, 2])
      assert List.starts_with?([1, 2, 3], [1, 2])
      refute List.starts_with?([1, 2, 3], [1, 2, 3, 4])
    end

    test "list is empty" do
      refute List.starts_with?([], [1])
      refute List.starts_with?([], [1, 2])
    end

    test "prefix is empty" do
      assert List.starts_with?([1], [])
      assert List.starts_with?([1, 2], [])
      assert List.starts_with?([1, 2, 3], [])
    end

    test "only accepts proper lists" do
      message = "no function clause matching in List.starts_with?/2"

      assert_raise FunctionClauseError, message, fn ->
        List.starts_with?([1 | 2], [1 | 2])
      end

      message = "no function clause matching in List.starts_with?/2"

      assert_raise FunctionClauseError, message, fn ->
        List.starts_with?([1, 2], 1)
      end
    end
  end

  test "to_string/1" do
    assert List.to_string([?æ, ?ß]) == "æß"
    assert List.to_string([?a, ?b, ?c]) == "abc"

    assert_raise UnicodeConversionError, "invalid code point 57343", fn ->
      List.to_string([0xDFFF])
    end

    assert_raise UnicodeConversionError, "invalid encoding starting at <<216, 0>>", fn ->
      List.to_string(["a", "b", <<0xD800::size(16)>>])
    end

    assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
      List.to_string([:a, :b])
    end
  end

  describe "myers_difference/2" do
    test "follows paper implementation" do
      assert List.myers_difference([], []) == []
      assert List.myers_difference([], [1, 2, 3]) == [ins: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], []) == [del: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], [1, 2, 3]) == [eq: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], [1, 4, 2, 3]) == [eq: [1], ins: [4], eq: [2, 3]]
      assert List.myers_difference([1, 4, 2, 3], [1, 2, 3]) == [eq: [1], del: [4], eq: [2, 3]]
      assert List.myers_difference([1], [[1]]) == [del: [1], ins: [[1]]]
      assert List.myers_difference([[1]], [1]) == [del: [[1]], ins: [1]]
    end

    test "rearranges inserts and equals for smaller diffs" do
      assert List.myers_difference([3, 2, 0, 2], [2, 2, 0, 2]) ==
               [del: [3], ins: [2], eq: [2, 0, 2]]

      assert List.myers_difference([3, 2, 1, 0, 2], [2, 1, 2, 1, 0, 2]) ==
               [del: [3], ins: [2, 1], eq: [2, 1, 0, 2]]

      assert List.myers_difference([3, 2, 2, 1, 0, 2], [2, 2, 1, 2, 1, 0, 2]) ==
               [del: [3], eq: [2, 2, 1], ins: [2, 1], eq: [0, 2]]

      assert List.myers_difference([3, 2, 0, 2], [2, 2, 1, 0, 2]) ==
               [del: [3], eq: [2], ins: [2, 1], eq: [0, 2]]
    end
  end

  test "improper?/1" do
    assert List.improper?([1 | 2])
    assert List.improper?([1, 2, 3 | 4])
    refute List.improper?([])
    refute List.improper?([1])
    refute List.improper?([[1]])
    refute List.improper?([1, 2])
    refute List.improper?([1, 2, 3])

    assert_raise FunctionClauseError, fn ->
      List.improper?(%{})
    end
  end

  describe "ascii_printable?/1" do
    test "proper lists without limit" do
      assert List.ascii_printable?([])
      assert List.ascii_printable?('abc')
      refute(List.ascii_printable?('abc' ++ [0]))
      assert List.ascii_printable?('abc \a\b\e\f\n\r\t\v def')
      refute List.ascii_printable?('mañana')
    end

    test "proper lists with limit" do
      assert List.ascii_printable?([], 100)
      assert List.ascii_printable?('abc' ++ [0], 2)
    end

    test "improper lists" do
      refute List.ascii_printable?('abc' ++ ?d)
      assert List.ascii_printable?('abc' ++ ?d, 3)
    end
  end
end

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

defmodule ListTest do
  use ExUnit.Case, async: true

  doctest List

  test "cons cell precedence" do
    assert [1 | List.flatten([2, 3])] == [1, 2, 3]
  end

  test "optional comma" do
    assert Code.eval_string("[1,]") == {[1], []}
    assert Code.eval_string("[1, 2, 3,]") == {[1, 2, 3], []}
  end

  test "partial application" do
    assert (&[&1, 2]).(1) == [1, 2]
    assert (&[&1, &2]).(1, 2) == [1, 2]
    assert (&[&2, &1]).(2, 1) == [1, 2]
    assert (&[&1 | &2]).(1, 2) == [1 | 2]
    assert (&[&1, &2 | &3]).(1, 2, 3) == [1, 2 | 3]
  end

  test "wrap/1" do
    assert List.wrap([1, 2, 3]) == [1, 2, 3]
    assert List.wrap(1) == [1]
    assert List.wrap(nil) == []
  end

  test "flatten/1" do
    assert List.flatten([1, 2, 3]) == [1, 2, 3]
    assert List.flatten([1, [2], 3]) == [1, 2, 3]
    assert List.flatten([[1, [2], 3]]) == [1, 2, 3]

    assert List.flatten([]) == []
    assert List.flatten([[]]) == []
  end

  test "flatten/2" do
    assert List.flatten([1, 2, 3], [4, 5]) == [1, 2, 3, 4, 5]
    assert List.flatten([1, [2], 3], [4, 5]) == [1, 2, 3, 4, 5]
    assert List.flatten([[1, [2], 3]], [4, 5]) == [1, 2, 3, 4, 5]
  end

  test "foldl/3" do
    assert List.foldl([1, 2, 3], 0, fn x, y -> x + y end) == 6
    assert List.foldl([1, 2, 3], 10, fn x, y -> x + y end) == 16
    assert List.foldl([1, 2, 3, 4], 0, fn x, y -> x - y end) == 2
  end

  test "foldr/3" do
    assert List.foldr([1, 2, 3], 0, fn x, y -> x + y end) == 6
    assert List.foldr([1, 2, 3], 10, fn x, y -> x + y end) == 16
    assert List.foldr([1, 2, 3, 4], 0, fn x, y -> x - y end) == -2
  end

  test "duplicate/2" do
    assert List.duplicate(1, 3) == [1, 1, 1]
    assert List.duplicate([1], 1) == [[1]]
  end

  test "last/1" do
    assert List.last([]) == nil
    assert List.last([1]) == 1
    assert List.last([1, 2, 3]) == 3
  end

  test "zip/1" do
    assert List.zip([[1, 4], [2, 5], [3, 6]]) == [{1, 2, 3}, {4, 5, 6}]
    assert List.zip([[1, 4], [2, 5, 0], [3, 6]]) == [{1, 2, 3}, {4, 5, 6}]
    assert List.zip([[1], [2, 5], [3, 6]]) == [{1, 2, 3}]
    assert List.zip([[1, 4], [2, 5], []]) == []
  end

  test "keyfind/4" do
    assert List.keyfind([a: 1, b: 2], :a, 0) == {:a, 1}
    assert List.keyfind([a: 1, b: 2], 2, 1) == {:b, 2}
    assert List.keyfind([a: 1, b: 2], :c, 0) == nil
  end

  test "keyreplace/4" do
    assert List.keyreplace([a: 1, b: 2], :a, 0, {:a, 3}) == [a: 3, b: 2]
    assert List.keyreplace([a: 1], :b, 0, {:b, 2}) == [a: 1]
  end

  test "keysort/2" do
    assert List.keysort([a: 4, b: 3, c: 5], 1) == [b: 3, a: 4, c: 5]
    assert List.keysort([a: 4, c: 1, b: 2], 0) == [a: 4, b: 2, c: 1]
  end

  test "keystore/4" do
    assert List.keystore([a: 1, b: 2], :a, 0, {:a, 3}) == [a: 3, b: 2]
    assert List.keystore([a: 1], :b, 0, {:b, 2}) == [a: 1, b: 2]
  end

  test "keymember?/3" do
    assert List.keymember?([a: 1, b: 2], :a, 0) == true
    assert List.keymember?([a: 1, b: 2], 2, 1) == true
    assert List.keymember?([a: 1, b: 2], :c, 0) == false
  end

  test "keydelete/3" do
    assert List.keydelete([a: 1, b: 2], :a, 0) == [{:b, 2}]
    assert List.keydelete([a: 1, b: 2], 2, 1) == [{:a, 1}]
    assert List.keydelete([a: 1, b: 2], :c, 0) == [{:a, 1}, {:b, 2}]
  end

  test "insert_at/3" do
    assert List.insert_at([1, 2, 3], 0, 0) == [0, 1, 2, 3]
    assert List.insert_at([1, 2, 3], 3, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], 2, 0) == [1, 2, 0, 3]
    assert List.insert_at([1, 2, 3], 10, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], -1, 0) == [1, 2, 3, 0]
    assert List.insert_at([1, 2, 3], -4, 0) == [0, 1, 2, 3]
    assert List.insert_at([1, 2, 3], -10, 0) == [0, 1, 2, 3]
  end

  test "replace_at/3" do
    assert List.replace_at([1, 2, 3], 0, 0) == [0, 2, 3]
    assert List.replace_at([1, 2, 3], 1, 0) == [1, 0, 3]
    assert List.replace_at([1, 2, 3], 2, 0) == [1, 2, 0]
    assert List.replace_at([1, 2, 3], 3, 0) == [1, 2, 3]
    assert List.replace_at([1, 2, 3], -1, 0) == [1, 2, 0]
    assert List.replace_at([1, 2, 3], -4, 0) == [1, 2, 3]
  end

  test "update_at/3" do
    assert List.update_at([1, 2, 3], 0, &(&1 + 1)) == [2, 2, 3]
    assert List.update_at([1, 2, 3], 1, &(&1 + 1)) == [1, 3, 3]
    assert List.update_at([1, 2, 3], 2, &(&1 + 1)) == [1, 2, 4]
    assert List.update_at([1, 2, 3], 3, &(&1 + 1)) == [1, 2, 3]
    assert List.update_at([1, 2, 3], -1, &(&1 + 1)) == [1, 2, 4]
    assert List.update_at([1, 2, 3], -4, &(&1 + 1)) == [1, 2, 3]
  end

  test "delete_at/2" do
    for index <- [-1, 0, 1] do
      assert List.delete_at([], index) == []
    end

    assert List.delete_at([1, 2, 3], 0) == [2, 3]
    assert List.delete_at([1, 2, 3], 2) == [1, 2]
    assert List.delete_at([1, 2, 3], 3) == [1, 2, 3]
    assert List.delete_at([1, 2, 3], -1) == [1, 2]
    assert List.delete_at([1, 2, 3], -3) == [2, 3]
    assert List.delete_at([1, 2, 3], -4) == [1, 2, 3]
  end

  test "pop_at/3" do
    for index <- [-1, 0, 1] do
      assert List.pop_at([], index) == {nil, []}
    end

    assert List.pop_at([1], 1, 2) == {2, [1]}
    assert List.pop_at([1, 2, 3], 0) == {1, [2, 3]}
    assert List.pop_at([1, 2, 3], 2) == {3, [1, 2]}
    assert List.pop_at([1, 2, 3], 3) == {nil, [1, 2, 3]}
    assert List.pop_at([1, 2, 3], -1) == {3, [1, 2]}
    assert List.pop_at([1, 2, 3], -3) == {1, [2, 3]}
    assert List.pop_at([1, 2, 3], -4) == {nil, [1, 2, 3]}
  end

  describe "starts_with?/2" do
    test "list and prefix are equal" do
      assert List.starts_with?([], [])
      assert List.starts_with?([1], [1])
      assert List.starts_with?([1, 2, 3], [1, 2, 3])
    end

    test "proper lists" do
      refute List.starts_with?([1], [1, 2])
      assert List.starts_with?([1, 2, 3], [1, 2])
      refute List.starts_with?([1, 2, 3], [1, 2, 3, 4])
    end

    test "list is empty" do
      refute List.starts_with?([], [1])
      refute List.starts_with?([], [1, 2])
    end

    test "prefix is empty" do
      assert List.starts_with?([1], [])
      assert List.starts_with?([1, 2], [])
      assert List.starts_with?([1, 2, 3], [])
    end

    test "only accepts proper lists" do
      message = "no function clause matching in List.starts_with?/2"

      assert_raise FunctionClauseError, message, fn ->
        List.starts_with?([1 | 2], [1 | 2])
      end

      message = "no function clause matching in List.starts_with?/2"

      assert_raise FunctionClauseError, message, fn ->
        List.starts_with?([1, 2], 1)
      end
    end
  end

  test "to_string/1" do
    assert List.to_string([?æ, ?ß]) == "æß"
    assert List.to_string([?a, ?b, ?c]) == "abc"

    assert_raise UnicodeConversionError, "invalid code point 57343", fn ->
      List.to_string([0xDFFF])
    end

    assert_raise UnicodeConversionError, "invalid encoding starting at <<216, 0>>", fn ->
      List.to_string(["a", "b", <<0xD800::size(16)>>])
    end

    assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn ->
      List.to_string([:a, :b])
    end
  end

  describe "myers_difference/2" do
    test "follows paper implementation" do
      assert List.myers_difference([], []) == []
      assert List.myers_difference([], [1, 2, 3]) == [ins: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], []) == [del: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], [1, 2, 3]) == [eq: [1, 2, 3]]
      assert List.myers_difference([1, 2, 3], [1, 4, 2, 3]) == [eq: [1], ins: [4], eq: [2, 3]]
      assert List.myers_difference([1, 4, 2, 3], [1, 2, 3]) == [eq: [1], del: [4], eq: [2, 3]]
      assert List.myers_difference([1], [[1]]) == [del: [1], ins: [[1]]]
      assert List.myers_difference([[1]], [1]) == [del: [[1]], ins: [1]]
    end

    test "rearranges inserts and equals for smaller diffs" do
      assert List.myers_difference([3, 2, 0, 2], [2, 2, 0, 2]) ==
               [del: [3], ins: [2], eq: [2, 0, 2]]

      assert List.myers_difference([3, 2, 1, 0, 2], [2, 1, 2, 1, 0, 2]) ==
               [del: [3], ins: [2, 1], eq: [2, 1, 0, 2]]

      assert List.myers_difference([3, 2, 2, 1, 0, 2], [2, 2, 1, 2, 1, 0, 2]) ==
               [del: [3], eq: [2, 2, 1], ins: [2, 1], eq: [0, 2]]

      assert List.myers_difference([3, 2, 0, 2], [2, 2, 1, 0, 2]) ==
               [del: [3], eq: [2], ins: [2, 1], eq: [0, 2]]
    end
  end

  test "improper?/1" do
    assert List.improper?([1 | 2])
    assert List.improper?([1, 2, 3 | 4])
    refute List.improper?([])
    refute List.improper?([1])
    refute List.improper?([[1]])
    refute List.improper?([1, 2])
    refute List.improper?([1, 2, 3])

    assert_raise FunctionClauseError, fn ->
      List.improper?(%{})
    end
  end

  describe "ascii_printable?/1" do
    test "proper lists without limit" do
      assert List.ascii_printable?([])
      assert List.ascii_printable?('abc')
      refute(List.ascii_printable?('abc' ++ [0]))
      assert List.ascii_printable?('abc \a\b\e\f\n\r\t\v def')
      refute List.ascii_printable?('mañana')
    end

    test "proper lists with limit" do
      assert List.ascii_printable?([], 100)
      assert List.ascii_printable?('abc' ++ [0], 2)
    end

    test "improper lists" do
      refute List.ascii_printable?('abc' ++ ?d)
    end
  end
end


worker delay

worker delay

defmodule IascSubastas.SubastaWorker do
  use GenServer
  use IascSubastas.Web, :worker

  require Logger
  import BlockTimer
  alias IascSubastas.Subasta
  alias Ecto.DateTime
  # Callbacks

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

  def init(_args) do
    subastas = Repo.all from s in Subasta,
                        preload: [:mejor_oferta],
                        where: s.terminada == false
    # TODO: sumar 5 segundos a la duración
    Logger.info"SubastaWorker> Empezando..."
    Enum.each(
      subastas,
      &tiempo_subasta_caida/1
    )
    {:ok, Enum.map(subastas, fn(s) -> s.id end)}
  end

  def handle_call(_mensaje, _from, subastas) do
    {:reply, :ok, subastas}
  end

  def handle_cast({:nueva_subasta, subasta_id}, subastas) do
    subasta = Repo.get!(Subasta, subasta_id) |> Repo.preload(:mejor_oferta)
    tiempo_subasta(subasta, 0)
    {:noreply, [subasta_id|subastas] }
  end

  def handle_cast({:cancela_subasta, subasta_id}, subastas) do
    termina_subasta(subasta_id)
    {:noreply, List.delete(subastas, subasta_id)}
  end

  def tiempo_subasta_caida(subasta) do
    tiempo_subasta(subasta, 5)
  end

  def tiempo_subasta(subasta, extra) do
    delay = subasta.duracion + extra
    Logger.info"SubastaWorker> La subasta #{subasta.id} termina en #{10} segundos..."
    apply_after 10 |> seconds, do: termina_subasta(subasta.id)
  end

  def termina_subasta(subasta_id) do
    subasta = Repo.get!(Subasta, subasta_id) |> Repo.preload(:mejor_oferta)
    changeset = Subasta.changeset(subasta, %{terminada: true})
    Repo.update(changeset)
    case subasta.mejor_oferta do
      nil ->
        Logger.info"SubastaWorker> terminó la subasta #{subasta_id}, nadie ganó..."
      mejor_oferta ->
        Logger.info"SubastaWorker> terminó la subasta #{subasta_id}, ganó #{mejor_oferta.comprador}..."
    end
  end
end

defmodule IascSubastas.SubastaWorker do
  use GenServer
  use IascSubastas.Web, :worker

  require Logger
  import BlockTimer
  alias IascSubastas.Subasta
  alias Ecto.DateTime
  # Callbacks

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

  def init(_args) do
    subastas = Repo.all from s in Subasta,
                        preload: [:mejor_oferta],
                        where: s.terminada == false
    # TODO: sumar 5 segundos a la duración
    Logger.info"SubastaWorker> Empezando..."
    Enum.each(
      subastas,
      &tiempo_subasta_caida/1
    )
    {:ok, Enum.map(subastas, fn(s) -> s.id end)}
  end

  def handle_call(_mensaje, _from, subastas) do
    {:reply, :ok, subastas}
  end

  def handle_cast({:nueva_subasta, subasta_id}, subastas) do
    subasta = Repo.get!(Subasta, subasta_id) |> Repo.preload(:mejor_oferta)
    tiempo_subasta(subasta, 0)
    {:noreply, [subasta_id|subastas] }
  end

  def handle_cast({:cancela_subasta, subasta_id}, subastas) do
    {:noreply, List.delete(subastas, subasta_id)}
  end

  def tiempo_subasta_caida(subasta) do
    tiempo_subasta(subasta, 5)
  end

  def tiempo_subasta(subasta, extra) do
    delay = subasta.duracion + extra
    Logger.info"SubastaWorker> La subasta #{subasta.id} termina en #{delay} segundos..."
    apply_after delay |> seconds, do: termina_subasta(subasta.id)
  end

  def termina_subasta(subasta_id) do
    Logger.info"SubastaWorker> terminó la subasta #{subasta_id}..."
  end
end


Fix formatting

Fix formatting

More spaces were needed to have the intended effect of formatting the
code samples.

defmodule Mix.Tasks.Nerves.Release.Init do
  use Mix.Task

  @shortdoc "Prepare a new project for use with releases"

  @moduledoc """
  Prepares a new project for use with releases.
  By default, this forwards the call to
      mix release.init --template /path/to/nerves/release_template.eex

  For more information on additional args, see
      mix help release.init
  """

  @spec run(OptionParser.argv) :: no_return
  def run(args) do
    template_path = Path.join(["#{:code.priv_dir(:nerves)}", "templates", "release.eex"])
    Mix.Task.run("release.init", args ++ ["--template", template_path])
  end
end

defmodule Mix.Tasks.Nerves.Release.Init do
  use Mix.Task

  @shortdoc "Prepare a new project for use with releases"

  @moduledoc """
  Prepares a new project for use with releases.
  By default, this forwards the call to
    mix release.init --template /path/to/nerves/release_template.eex

  For more information on additional args, reference
    mix help release.init
  """

  @spec run(OptionParser.argv) :: no_return
  def run(args) do
    template_path = Path.join(["#{:code.priv_dir(:nerves)}", "templates", "release.eex"])
    Mix.Task.run("release.init", args ++ ["--template", template_path])
  end
end


Remove unnecessary functions from release tasks

# https://hexdocs.pm/distillery/running-migrations.html
defmodule PhoenixWebsite.ReleaseTasks do
  @start_apps [
    :postgrex,
    :ecto
  ]

  @myapps [
    :phoenix_website
  ]

  @repos [
    PhoenixWebsite.Repo
  ]

  def seed do
    run_seed_file("seeds")
  end

  def run_seed_file(seed_file_name) do
    IO.puts "Loading myapp.."
    # Load the code for myapp, but don't start it
    :ok = Application.load(:phoenix_website)

    IO.puts "Starting dependencies.."
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)

    # Start the Repo(s) for myapp
    IO.puts "Starting repos.."
    Enum.each(@repos, &(&1.start_link(pool_size: 1)))

    # Run migrations
    Enum.each(@myapps, &run_migrations_for/1)

    # Run the seed script if it exists
    seed_script = Path.join([priv_dir(:phoenix_website), "repo", "#{seed_file_name}.exs"])
    if File.exists?(seed_script) do
      IO.puts "Running seed script.."
      Code.eval_file(seed_script)
    end

    # Signal shutdown
    IO.puts "Success!"
    :init.stop()
  end

  def priv_dir(app), do: "#{:code.priv_dir(app)}"

  defp run_migrations_for(app) do
    IO.puts "Running migrations for #{app}"
    Ecto.Migrator.run(Ieep.Repo, migrations_path(app), :up, all: true)
  end

  defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
  defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])
end

# https://hexdocs.pm/distillery/running-migrations.html
defmodule PhoenixWebsite.ReleaseTasks do
  @start_apps [
    :postgrex,
    :ecto
  ]

  @myapps [
    :phoenix_website
  ]

  @repos [
    PhoenixWebsite.Repo
  ]

  def seed do
    run_seed_file("seeds")
  end

  def sample_dev_seeds do
    run_seed_file("sample_dev_seeds")
  end

  def import_production_data do
    run_seed_file("import_production_data")
  end

  def run_seed_file(seed_file_name) do
    IO.puts "Loading myapp.."
    # Load the code for myapp, but don't start it
    :ok = Application.load(:phoenix_website)

    IO.puts "Starting dependencies.."
    # Start apps necessary for executing migrations
    Enum.each(@start_apps, &Application.ensure_all_started/1)

    # Start the Repo(s) for myapp
    IO.puts "Starting repos.."
    Enum.each(@repos, &(&1.start_link(pool_size: 1)))

    # Run migrations
    Enum.each(@myapps, &run_migrations_for/1)

    # Run the seed script if it exists
    seed_script = Path.join([priv_dir(:phoenix_website), "repo", "#{seed_file_name}.exs"])
    if File.exists?(seed_script) do
      IO.puts "Running seed script.."
      Code.eval_file(seed_script)
    end

    # Signal shutdown
    IO.puts "Success!"
    :init.stop()
  end

  def priv_dir(app), do: "#{:code.priv_dir(app)}"

  defp run_migrations_for(app) do
    IO.puts "Running migrations for #{app}"
    Ecto.Migrator.run(Ieep.Repo, migrations_path(app), :up, all: true)
  end

  defp migrations_path(app), do: Path.join([priv_dir(app), "repo", "migrations"])
  defp seed_path(app), do: Path.join([priv_dir(app), "repo", "seeds.exs"])
end


Update broken link from Token API to IAM docs


Remove function injection from Selenium driver (#506)

This injection was only for testing and we've since migrated
to bypass based tests.

defmodule Wallaby.Experimental.Selenium do
  @moduledoc """
  The Selenium driver uses [Selenium Server](https://github.com/SeleniumHQ/selenium) to power many types of browsers (Chrome, Firefox, Edge, etc).

  ## Usage

  Start a Wallaby Session using this driver with the following command:

  ```
  {:ok, session} = Wallaby.start_session()
  ```

  ## Configuration

  ### Capabilities

  These capabilities will override the default capabilities.

  ```
  config :wallaby,
    selenium: [
      capabilities: %{
        # something
      }
    ]
  ```

  ## Default Capabilities

  By default, Selenium will use the following capabilities

  You can read more about capabilities in the [JSON Wire Protocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#capabilities-json-object) documentation.

  ```elixir
  %{
    javascriptEnabled: true,
    browserName: "firefox",
    "moz:firefoxOptions": %{
      args: ["-headless"]
    }
  }
  ```

  ## Notes

  - Requires [selenium-server-standalone](https://www.seleniumhq.org/download/) to be running on port 4444. Wallaby does _not_ manage the start/stop of the Selenium server.
  - Requires [GeckoDriver](https://github.com/mozilla/geckodriver) to be installed in your path when using [Firefox](https://www.mozilla.org/en-US/firefox/new/). Firefox is used by default.
  """

  use Supervisor

  @behaviour Wallaby.Driver

  alias Wallaby.{Driver, Element, Session}
  alias Wallaby.Experimental.Selenium.WebdriverClient

  @typedoc """
  Options to pass to Wallaby.start_session/1

  ```elixir
  Wallaby.start_session(
    remote_url: "http://selenium_url",
    capabilities: %{browserName: "firefox"}
  )
  ```
  """
  @type start_session_opts ::
          {:remote_url, String.t()}
          | {:capabilities, map}

  @doc false
  def start_link(opts \\ []) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @doc false
  def init(:ok) do
    supervise([], strategy: :one_for_one)
  end

  @doc false
  def validate do
    :ok
  end

  @doc false
  @spec start_session([start_session_opts]) :: Wallaby.Driver.on_start_session() | no_return
  def start_session(opts \\ []) do
    base_url = Keyword.get(opts, :remote_url, "http://localhost:4444/wd/hub/")
    capabilities = Keyword.get(opts, :capabilities, capabilities_from_config())

    with {:ok, response} <- WebdriverClient.create_session(base_url, capabilities) do
      id = response["sessionId"]

      session = %Wallaby.Session{
        session_url: base_url <> "session/#{id}",
        url: base_url <> "session/#{id}",
        id: id,
        driver: __MODULE__
      }

      if window_size = Keyword.get(opts, :window_size),
        do: {:ok, _} = set_window_size(session, window_size[:width], window_size[:height])

      {:ok, session}
    end
  end

  defp capabilities_from_config() do
    :wallaby
    |> Application.get_env(:selenium, [])
    |> Keyword.get(:capabilities, default_capabilities())
  end

  @doc false
  @spec end_session(Session.t()) :: :ok
  def end_session(session) do
    WebdriverClient.delete_session(session)
    :ok
  end

  @doc false
  def blank_page?(session) do
    case current_url(session) do
      {:ok, url} ->
        url == "about:blank"

      _ ->
        false
    end
  end

  @doc false
  defdelegate window_handle(session), to: WebdriverClient
  @doc false
  defdelegate window_handles(session), to: WebdriverClient
  @doc false
  defdelegate focus_window(session, window_handle), to: WebdriverClient
  @doc false
  defdelegate close_window(session), to: WebdriverClient
  @doc false
  defdelegate get_window_size(session), to: WebdriverClient
  @doc false
  defdelegate set_window_size(session, width, height), to: WebdriverClient
  @doc false
  defdelegate get_window_position(session), to: WebdriverClient
  @doc false
  defdelegate set_window_position(session, x, y), to: WebdriverClient
  @doc false
  defdelegate maximize_window(session), to: WebdriverClient

  @doc false
  defdelegate focus_frame(session, frame), to: WebdriverClient
  @doc false
  defdelegate focus_parent_frame(session), to: WebdriverClient

  @doc false
  defdelegate accept_alert(session, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_alert(session, fun), to: WebdriverClient
  @doc false
  defdelegate accept_confirm(session, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_confirm(session, fun), to: WebdriverClient
  @doc false
  defdelegate accept_prompt(session, input, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_prompt(session, fun), to: WebdriverClient

  @doc false
  defdelegate take_screenshot(session_or_element), to: WebdriverClient

  @doc false
  def cookies(%Session{} = session) do
    WebdriverClient.cookies(session)
  end

  @doc false
  def current_path(%Session{} = session) do
    with {:ok, url} <- WebdriverClient.current_url(session),
         uri <- URI.parse(url),
         {:ok, path} <- Map.fetch(uri, :path),
         do: {:ok, path}
  end

  @doc false
  def current_url(%Session{} = session) do
    WebdriverClient.current_url(session)
  end

  @doc false
  def page_source(%Session{} = session) do
    WebdriverClient.page_source(session)
  end

  @doc false
  def page_title(%Session{} = session) do
    WebdriverClient.page_title(session)
  end

  @doc false
  def set_cookie(%Session{} = session, key, value) do
    WebdriverClient.set_cookie(session, key, value)
  end

  @doc false
  def visit(%Session{} = session, path) do
    WebdriverClient.visit(session, path)
  end

  @doc false
  def attribute(%Element{} = element, name) do
    WebdriverClient.attribute(element, name)
  end

  @doc false
  @spec clear(Element.t()) :: {:ok, nil} | {:error, Driver.reason()}
  def clear(%Element{} = element) do
    WebdriverClient.clear(element)
  end

  @doc false
  def click(%Element{} = element) do
    WebdriverClient.click(element)
  end

  @doc false
  def click(parent, button) do
    WebdriverClient.click(parent, button)
  end

  @doc false
  def button_down(parent, button) do
    WebdriverClient.button_down(parent, button)
  end

  @doc false
  def button_up(parent, button) do
    WebdriverClient.button_up(parent, button)
  end

  @doc false
  def double_click(parent) do
    WebdriverClient.double_click(parent)
  end

  @doc false
  def hover(%Element{} = element) do
    WebdriverClient.move_mouse_to(nil, element)
  end

  @doc false
  def move_mouse_by(session, x_offset, y_offset) do
    WebdriverClient.move_mouse_to(session, nil, x_offset, y_offset)
  end

  @doc false
  def displayed(%Element{} = element) do
    WebdriverClient.displayed(element)
  end

  @doc false
  def selected(%Element{} = element) do
    WebdriverClient.selected(element)
  end

  @doc false
  @spec set_value(Element.t(), String.t()) :: {:ok, nil} | {:error, Driver.reason()}
  def set_value(%Element{} = element, value) do
    WebdriverClient.set_value(element, value)
  end

  @doc false
  def text(%Element{} = element) do
    WebdriverClient.text(element)
  end

  @doc false
  def find_elements(parent, compiled_query) do
    WebdriverClient.find_elements(parent, compiled_query)
  end

  @doc false
  def execute_script(parent, script, arguments \\ []) do
    WebdriverClient.execute_script(parent, script, arguments)
  end

  @doc false
  def execute_script_async(parent, script, arguments \\ []) do
    WebdriverClient.execute_script_async(parent, script, arguments)
  end

  @doc false
  def send_keys(parent, keys) do
    WebdriverClient.send_keys(parent, keys)
  end

  defp default_capabilities do
    %{
      javascriptEnabled: true,
      browserName: "firefox",
      "moz:firefoxOptions": %{
        args: ["-headless"]
      }
    }
  end
end

defmodule Wallaby.Experimental.Selenium do
  @moduledoc """
  The Selenium driver uses [Selenium Server](https://github.com/SeleniumHQ/selenium) to power many types of browsers (Chrome, Firefox, Edge, etc).

  ## Usage

  Start a Wallaby Session using this driver with the following command:

  ```
  {:ok, session} = Wallaby.start_session()
  ```

  ## Configuration

  ### Capabilities

  These capabilities will override the default capabilities.

  ```
  config :wallaby,
    selenium: [
      capabilities: %{
        # something
      }
    ]
  ```

  ## Default Capabilities

  By default, Selenium will use the following capabilities

  You can read more about capabilities in the [JSON Wire Protocol](https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#capabilities-json-object) documentation.

  ```elixir
  %{
    javascriptEnabled: true,
    browserName: "firefox",
    "moz:firefoxOptions": %{
      args: ["-headless"]
    }
  }
  ```

  ## Notes

  - Requires [selenium-server-standalone](https://www.seleniumhq.org/download/) to be running on port 4444. Wallaby does _not_ manage the start/stop of the Selenium server.
  - Requires [GeckoDriver](https://github.com/mozilla/geckodriver) to be installed in your path when using [Firefox](https://www.mozilla.org/en-US/firefox/new/). Firefox is used by default.
  """

  use Supervisor

  @behaviour Wallaby.Driver

  alias Wallaby.{Driver, Element, Session}
  alias Wallaby.Experimental.Selenium.WebdriverClient

  @typedoc """
  Options to pass to Wallaby.start_session/1

  ```elixir
  Wallaby.start_session(
    remote_url: "http://selenium_url",
    capabilities: %{browserName: "firefox"},
    create_session_fn: fn url, capabilities ->
      WebdriverClient.create_session(url, capabilities)
    end
  )
  ```
  """
  @type start_session_opts ::
          {:remote_url, String.t()}
          | {:capabilities, map}
          | {:create_session_fn, (String.t(), map -> {:ok, %{}})}

  @typedoc false
  @type end_session_opts :: {:end_session_fn, (Session.t() -> any)}

  @doc false
  def start_link(opts \\ []) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @doc false
  def init(:ok) do
    supervise([], strategy: :one_for_one)
  end

  @doc false
  def validate do
    :ok
  end

  @doc false
  @spec start_session([start_session_opts]) :: Wallaby.Driver.on_start_session() | no_return
  def start_session(opts \\ []) do
    base_url = Keyword.get(opts, :remote_url, "http://localhost:4444/wd/hub/")
    create_session_fn = Keyword.get(opts, :create_session_fn, &WebdriverClient.create_session/2)

    capabilities = Keyword.get(opts, :capabilities, capabilities_from_config())

    with {:ok, response} <- create_session_fn.(base_url, capabilities) do
      id = response["sessionId"]

      session = %Wallaby.Session{
        session_url: base_url <> "session/#{id}",
        url: base_url <> "session/#{id}",
        id: id,
        driver: __MODULE__
      }

      if window_size = Keyword.get(opts, :window_size),
        do: {:ok, _} = set_window_size(session, window_size[:width], window_size[:height])

      {:ok, session}
    end
  end

  defp capabilities_from_config() do
    :wallaby
    |> Application.get_env(:selenium, [])
    |> Keyword.get(:capabilities, default_capabilities())
  end

  @doc false
  @spec end_session(Session.t(), [end_session_opts]) :: :ok
  def end_session(session, opts \\ []) do
    end_session_fn = Keyword.get(opts, :end_session_fn, &WebdriverClient.delete_session/1)

    end_session_fn.(session)
    :ok
  end

  @doc false
  def blank_page?(session) do
    case current_url(session) do
      {:ok, url} ->
        url == "about:blank"

      _ ->
        false
    end
  end

  @doc false
  defdelegate window_handle(session), to: WebdriverClient
  @doc false
  defdelegate window_handles(session), to: WebdriverClient
  @doc false
  defdelegate focus_window(session, window_handle), to: WebdriverClient
  @doc false
  defdelegate close_window(session), to: WebdriverClient
  @doc false
  defdelegate get_window_size(session), to: WebdriverClient
  @doc false
  defdelegate set_window_size(session, width, height), to: WebdriverClient
  @doc false
  defdelegate get_window_position(session), to: WebdriverClient
  @doc false
  defdelegate set_window_position(session, x, y), to: WebdriverClient
  @doc false
  defdelegate maximize_window(session), to: WebdriverClient

  @doc false
  defdelegate focus_frame(session, frame), to: WebdriverClient
  @doc false
  defdelegate focus_parent_frame(session), to: WebdriverClient

  @doc false
  defdelegate accept_alert(session, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_alert(session, fun), to: WebdriverClient
  @doc false
  defdelegate accept_confirm(session, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_confirm(session, fun), to: WebdriverClient
  @doc false
  defdelegate accept_prompt(session, input, fun), to: WebdriverClient
  @doc false
  defdelegate dismiss_prompt(session, fun), to: WebdriverClient

  @doc false
  defdelegate take_screenshot(session_or_element), to: WebdriverClient

  @doc false
  def cookies(%Session{} = session) do
    WebdriverClient.cookies(session)
  end

  @doc false
  def current_path(%Session{} = session) do
    with {:ok, url} <- WebdriverClient.current_url(session),
         uri <- URI.parse(url),
         {:ok, path} <- Map.fetch(uri, :path),
         do: {:ok, path}
  end

  @doc false
  def current_url(%Session{} = session) do
    WebdriverClient.current_url(session)
  end

  @doc false
  def page_source(%Session{} = session) do
    WebdriverClient.page_source(session)
  end

  @doc false
  def page_title(%Session{} = session) do
    WebdriverClient.page_title(session)
  end

  @doc false
  def set_cookie(%Session{} = session, key, value) do
    WebdriverClient.set_cookie(session, key, value)
  end

  @doc false
  def visit(%Session{} = session, path) do
    WebdriverClient.visit(session, path)
  end

  @doc false
  def attribute(%Element{} = element, name) do
    WebdriverClient.attribute(element, name)
  end

  @doc false
  @spec clear(Element.t()) :: {:ok, nil} | {:error, Driver.reason()}
  def clear(%Element{} = element) do
    WebdriverClient.clear(element)
  end

  @doc false
  def click(%Element{} = element) do
    WebdriverClient.click(element)
  end

  @doc false
  def click(parent, button) do
    WebdriverClient.click(parent, button)
  end

  @doc false
  def button_down(parent, button) do
    WebdriverClient.button_down(parent, button)
  end

  @doc false
  def button_up(parent, button) do
    WebdriverClient.button_up(parent, button)
  end

  @doc false
  def double_click(parent) do
    WebdriverClient.double_click(parent)
  end

  @doc false
  def hover(%Element{} = element) do
    WebdriverClient.move_mouse_to(nil, element)
  end

  @doc false
  def move_mouse_by(session, x_offset, y_offset) do
    WebdriverClient.move_mouse_to(session, nil, x_offset, y_offset)
  end

  @doc false
  def displayed(%Element{} = element) do
    WebdriverClient.displayed(element)
  end

  @doc false
  def selected(%Element{} = element) do
    WebdriverClient.selected(element)
  end

  @doc false
  @spec set_value(Element.t(), String.t()) :: {:ok, nil} | {:error, Driver.reason()}
  def set_value(%Element{} = element, value) do
    WebdriverClient.set_value(element, value)
  end

  @doc false
  def text(%Element{} = element) do
    WebdriverClient.text(element)
  end

  @doc false
  def find_elements(parent, compiled_query) do
    WebdriverClient.find_elements(parent, compiled_query)
  end

  @doc false
  def execute_script(parent, script, arguments \\ []) do
    WebdriverClient.execute_script(parent, script, arguments)
  end

  @doc false
  def execute_script_async(parent, script, arguments \\ []) do
    WebdriverClient.execute_script_async(parent, script, arguments)
  end

  @doc false
  def send_keys(parent, keys) do
    WebdriverClient.send_keys(parent, keys)
  end

  defp default_capabilities do
    %{
      javascriptEnabled: true,
      browserName: "firefox",
      "moz:firefoxOptions": %{
        args: ["-headless"]
      }
    }
  end
end


Remove obsolete test

Remove obsolete test


Return part of the body on message listing

require Logger

defmodule Serverboards.Notifications.InApp do
  @moduledoc """
  This module stores and allow manipulation of database stores notifications.

  This doe snot follow standard notification via plugin to ensure that no
  failure can prevent it from being saved. Also it is a mandatory channel.

  Other that that, stores as a normal Channel would do, and allow quering.
  """
  alias Serverboards.Repo
  alias Serverboards.Notifications.Model

  def setup_eventsourcing(es) do
    EventSourcing.subscribe es, :update, fn
      %{ id: id, tags: tags} = what, me ->
        update_real(id, what, me)
    end
  end

  def notify(email, subject, body, meta) do
    import Ecto.Query
    user_id = Repo.one( from u in Serverboards.Auth.Model.User, where: u.email == ^email, select: u.id )
    #Logger.debug("#{inspect meta}")

    tags = ["new","unread"]
    notification = %{ user_id: user_id, subject: subject, body: body, meta: meta, tags: tags }

    {:ok, notification } = Repo.insert( Model.Notification.changeset(
        %Model.Notification{}, notification
        ) )

    notification = %{ notification | inserted_at: Ecto.DateTime.to_iso8601(notification.inserted_at)}
    Serverboards.Event.emit("notifications.new", %{notification: notification}, %{ user: email, perms: ["notifications.list"] })

    :ok
  end

  def update(id, what, me) do
    EventSourcing.dispatch(
      :notifications, :update,
      %{ id: id } |> Map.merge(what),
      me.email)
  end

  def update_real(id, what, email) do
    me = Serverboards.Auth.User.user_info email, %{email: email}
    model = %Serverboards.Notifications.Model.Notification{}
      |> Map.merge(details(id, me))
      |> Map.put(:user_id, me.id)
    {:ok, notification} = Repo.update(Model.Notification.changeset( model, what ))
    #notification = %{ notification | inserted_at: Ecto.DateTime.to_iso8601(notification.inserted_at)}
    Serverboards.Event.emit("notifications.update", %{notification: notification}, %{ user: email, perms: ["notifications.list"] })
  end

  def list(filter, user) do
    import Ecto.Query
    q=   from m in Model.Notification,
        where: m.user_id == ^user.id,
     order_by: [desc: m.id],
       select: %{ subject: m.subject, tags: m.tags, inserted_at: m.inserted_at, id: m.id, body: m.body }

    # default values
    filter = Map.merge(%{"count" => 50}, filter)

    Logger.debug("#{inspect filter} #{inspect Map.to_list(filter)}")
    q = Enum.reduce(Map.to_list(filter), q, fn
      {"tags", tags}, q -> where(q, [m], fragment("tags @> ?", ^tags))
      {"count", count}, q -> limit(q, ^count)
      {"start", start}, q -> where(q, [m], m.start < ^start )

      {:tags, tags}, q -> where(q, [m], fragment("tags @> ?", ^tags))
      {:count, count}, q -> limit(q, ^count)
      {:start, start}, q -> where(q, [m], m.start < ^start )
    end)

    ret = Repo.all( q )
      |> Enum.map( fn n -> # post processing
        %{ n |
          inserted_at: Ecto.DateTime.to_iso8601(n.inserted_at),
          body: String.slice(n.body, 0, 512)
        }
      end)

    {:ok, ret}
  end

  def details(id, user) do
    import Ecto.Query
    n = Repo.one(
      from m in Model.Notification,
      where: (m.user_id == ^user.id) and (m.id == ^id),
      select: %{ subject: m.subject, body: m.body, tags: m.tags, meta: m.meta, inserted_at: m.inserted_at, id: m.id }
    )

    # post process
    %{ n | inserted_at: Ecto.DateTime.to_iso8601(n.inserted_at)}
  end

end

require Logger

defmodule Serverboards.Notifications.InApp do
  @moduledoc """
  This module stores and allow manipulation of database stores notifications.

  This doe snot follow standard notification via plugin to ensure that no
  failure can prevent it from being saved. Also it is a mandatory channel.

  Other that that, stores as a normal Channel would do, and allow quering.
  """
  alias Serverboards.Repo
  alias Serverboards.Notifications.Model

  def setup_eventsourcing(es) do
    EventSourcing.subscribe es, :update, fn
      %{ id: id, tags: tags} = what, me ->
        update_real(id, what, me)
    end
  end

  def notify(email, subject, body, meta) do
    import Ecto.Query
    user_id = Repo.one( from u in Serverboards.Auth.Model.User, where: u.email == ^email, select: u.id )
    #Logger.debug("#{inspect meta}")

    tags = ["new","unread"]
    notification = %{ user_id: user_id, subject: subject, body: body, meta: meta, tags: tags }

    {:ok, notification } = Repo.insert( Model.Notification.changeset(
        %Model.Notification{}, notification
        ) )

    notification = %{ notification | inserted_at: Ecto.DateTime.to_iso8601(notification.inserted_at)}
    Serverboards.Event.emit("notifications.new", %{notification: notification}, %{ user: email, perms: ["notifications.list"] })

    :ok
  end

  def update(id, what, me) do
    EventSourcing.dispatch(
      :notifications, :update,
      %{ id: id } |> Map.merge(what),
      me.email)
  end

  def update_real(id, what, email) do
    me = Serverboards.Auth.User.user_info email, %{email: email}
    model = %Serverboards.Notifications.Model.Notification{}
      |> Map.merge(details(id, me))
      |> Map.put(:user_id, me.id)
    {:ok, notification} = Repo.update(Model.Notification.changeset( model, what ))
    #notification = %{ notification | inserted_at: Ecto.DateTime.to_iso8601(notification.inserted_at)}
    Serverboards.Event.emit("notifications.update", %{notification: notification}, %{ user: email, perms: ["notifications.list"] })
  end

  def list(filter, user) do
    import Ecto.Query
    q=   from m in Model.Notification,
        where: m.user_id == ^user.id,
     order_by: [desc: m.id],
       select: %{ subject: m.subject, tags: m.tags, inserted_at: m.inserted_at, id: m.id }

    # default values
    filter = Map.merge(%{"count" => 50}, filter)

    Logger.debug("#{inspect filter} #{inspect Map.to_list(filter)}")
    q = Enum.reduce(Map.to_list(filter), q, fn
      {"tags", tags}, q -> where(q, [m], fragment("tags @> ?", ^tags))
      {"count", count}, q -> limit(q, ^count)
      {"start", start}, q -> where(q, [m], m.start < ^start )

      {:tags, tags}, q -> where(q, [m], fragment("tags @> ?", ^tags))
      {:count, count}, q -> limit(q, ^count)
      {:start, start}, q -> where(q, [m], m.start < ^start )
    end)

    ret = Repo.all( q )
      |> Enum.map( fn n -> # post processing
        %{ n | inserted_at: Ecto.DateTime.to_iso8601(n.inserted_at)}
      end)

    {:ok, ret}
  end

  def details(id, user) do
    import Ecto.Query
    n = Repo.one(
      from m in Model.Notification,
      where: (m.user_id == ^user.id) and (m.id == ^id),
      select: %{ subject: m.subject, body: m.body, tags: m.tags, meta: m.meta, inserted_at: m.inserted_at, id: m.id }
    )

    # post process
    %{ n | inserted_at: Ecto.DateTime.to_iso8601(n.inserted_at)}
  end

end