Do not violate http proxy adapter contract for responses with errors [patch]

defmodule Annon.Plugins.Proxy.Adapters.HTTP do
  @moduledoc """
  HTTP(s) requests adapter that sends body as-is via HTTPPoison.
  """
  alias Annon.Plugin.UpstreamRequest
  alias Plug.Conn
  require Logger

  @stream_opts [
    length: :infinity,      # Read whole response
    read_length: 1_049_600, # in 1 Mb chunks
    read_timeout: 15_000    # with 15 seconds timeout between chunks
  ]

  @buffer_opts [
    connect_timeout: 30_000,
    recv_timeout: 30_000,
    timeout: 30_000
  ]

  def dispatch(%UpstreamRequest{} = upstream_request, %Conn{body_params: %Plug.Conn.Unfetched{}} = conn) do
    upstream_url = UpstreamRequest.to_upstream_url!(upstream_request)
    method = String.to_atom(conn.method)

    with {:ok, client_ref} <- :hackney.request(method, upstream_url, upstream_request.headers, :stream, []),
         {:ok, client_ref, conn} <- stream_request_body(client_ref, upstream_request, conn),
         %Conn{} = conn <- stream_response_body(client_ref, conn) do
      {:ok, conn}
    else
      {:error, term} ->
        {:ok, Conn.send_resp(conn, 502, Annon.Helpers.Response.build_upstream_error(to_string(term)))}
    end
  end

  def dispatch(%UpstreamRequest{} = upstream_request, %Conn{method: method} = conn) do
    upstream_url = UpstreamRequest.to_upstream_url!(upstream_request)
    method = String.to_atom(method)

    body =
      conn
      |> Map.get(:body_params)
      |> Poison.encode!()

    case HTTPoison.request(method, upstream_url, body, upstream_request.headers, @buffer_opts) do
      {:ok, %{headers: resp_headers, status_code: resp_status_code, body: resp_body}} ->
        conn =
          conn
          |> put_response_headers(resp_headers)
          |> Conn.send_resp(resp_status_code, resp_body)

        {:ok, conn}
      {:error, %{reason: reason}} ->
        conn = Conn.send_resp(conn, 502, Annon.Helpers.Response.build_upstream_error(reason))
        {:ok, conn}
    end
  end

  defp stream_request_body(client_ref, upstream_request, conn) do
    case Conn.read_body(conn, @stream_opts) do
      {:ok, body, conn} ->
        :hackney.send_body(client_ref, body)
        {:ok, client_ref, conn}

      {:more, body, conn} ->
        :hackney.send_body(client_ref, body)
        stream_request_body(client_ref, upstream_request, conn)

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp stream_response_body(client_ref, conn) do
    case :hackney.start_response(client_ref) do
      {:ok, status_code, headers} ->
        conn
        |> put_response_headers(headers)
        |> Conn.send_resp(status_code, "")

      {:ok, status_code, headers, client_ref} ->
        conn
        |> put_response_headers(headers)
        |> Conn.send_chunked(status_code)
        |> stream_response_chunk(client_ref)

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp put_response_headers(conn, headers) do
    Enum.reduce(headers, conn, fn
      {"x-request-id", _header_value}, conn ->
        conn
      {header_key, header_value}, conn ->
        Conn.put_resp_header(conn, String.downcase(header_key), header_value)
    end)
  end

  defp stream_response_chunk(conn, client_ref) do
    case :hackney.stream_body(client_ref) do
      {:ok, body} ->
        {:ok, conn} = Conn.chunk(conn, body)
        stream_response_chunk(conn, client_ref)

      :done ->
        conn

      {:error, reason} ->
        {:error, reason}
    end
  end
end

defmodule Annon.Plugins.Proxy.Adapters.HTTP do
  @moduledoc """
  HTTP(s) requests adapter that sends body as-is via HTTPPoison.
  """
  alias Annon.Plugin.UpstreamRequest
  alias Plug.Conn
  require Logger

  @stream_opts [
    length: :infinity,      # Read whole response
    read_length: 1_049_600, # in 1 Mb chunks
    read_timeout: 15_000    # with 15 seconds timeout between chunks
  ]

  @buffer_opts [
    connect_timeout: 30_000,
    recv_timeout: 30_000,
    timeout: 30_000
  ]

  def dispatch(%UpstreamRequest{} = upstream_request, %Conn{body_params: %Plug.Conn.Unfetched{}} = conn) do
    upstream_url = UpstreamRequest.to_upstream_url!(upstream_request)
    method = String.to_atom(conn.method)

    with {:ok, client_ref} <- :hackney.request(method, upstream_url, upstream_request.headers, :stream, []),
         {:ok, client_ref, conn} <- stream_request_body(client_ref, upstream_request, conn),
         %Conn{} = conn <- stream_response_body(client_ref, conn) do
      {:ok, conn}
    else
      {:error, term} ->
        Conn.send_resp(conn, 502, Annon.Helpers.Response.build_upstream_error(to_string(term)))
    end
  end

  def dispatch(%UpstreamRequest{} = upstream_request, %Conn{method: method} = conn) do
    upstream_url = UpstreamRequest.to_upstream_url!(upstream_request)
    method = String.to_atom(method)

    body =
      conn
      |> Map.get(:body_params)
      |> Poison.encode!()

    case HTTPoison.request(method, upstream_url, body, upstream_request.headers, @buffer_opts) do
      {:ok, %{headers: resp_headers, status_code: resp_status_code, body: resp_body}} ->
        conn =
          conn
          |> put_response_headers(resp_headers)
          |> Conn.send_resp(resp_status_code, resp_body)

        {:ok, conn}
      {:error, %{reason: reason}} ->
        conn = Conn.send_resp(conn, 502, Annon.Helpers.Response.build_upstream_error(reason))
        {:ok, conn}
    end
  end

  defp stream_request_body(client_ref, upstream_request, conn) do
    case Conn.read_body(conn, @stream_opts) do
      {:ok, body, conn} ->
        :hackney.send_body(client_ref, body)
        {:ok, client_ref, conn}

      {:more, body, conn} ->
        :hackney.send_body(client_ref, body)
        stream_request_body(client_ref, upstream_request, conn)

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp stream_response_body(client_ref, conn) do
    case :hackney.start_response(client_ref) do
      {:ok, status_code, headers} ->
        conn
        |> put_response_headers(headers)
        |> Conn.send_resp(status_code, "")

      {:ok, status_code, headers, client_ref} ->
        conn
        |> put_response_headers(headers)
        |> Conn.send_chunked(status_code)
        |> stream_response_chunk(client_ref)

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp put_response_headers(conn, headers) do
    Enum.reduce(headers, conn, fn
      {"x-request-id", _header_value}, conn ->
        conn
      {header_key, header_value}, conn ->
        Conn.put_resp_header(conn, String.downcase(header_key), header_value)
    end)
  end

  defp stream_response_chunk(conn, client_ref) do
    case :hackney.stream_body(client_ref) do
      {:ok, body} ->
        {:ok, conn} = Conn.chunk(conn, body)
        stream_response_chunk(conn, client_ref)

      :done ->
        conn

      {:error, reason} ->
        {:error, reason}
    end
  end
end


redirect to my recommendations after recommendation/delete

defmodule PanWeb.RecommendationFrontendController do
  use PanWeb, :controller

  alias PanWeb.{Podcast, Recommendation, Subscription}

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

  def my_recommendations(conn, _params, user) do
    podcast_recommendations =
      Repo.all(
        from(r in Recommendation,
          where:
            r.user_id == ^user.id and
              not is_nil(r.podcast_id) and
              is_nil(r.episode_id) and
              is_nil(r.chapter_id),
          order_by: [desc: :inserted_at],
          preload: :podcast
        )
      )

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

    recommended_podcast_ids =
      Enum.map(podcast_recommendations, fn recommendation -> recommendation.podcast_id end)

    unrecommonded_podcast_ids =
      Enum.filter(subscribed_podcast_ids, fn id ->
        not Enum.member?(recommended_podcast_ids, id)
      end)

    unrecommended_podcasts =
      Repo.all(from(p in Podcast, where: p.id in ^unrecommonded_podcast_ids))

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "my_recommendations.html",
      podcast_recommendations: podcast_recommendations,
      unrecommended_podcasts: unrecommended_podcasts,
      changeset: changeset
    )
  end

  def create(conn, %{"recommendation" => recommendation_params}, user) do
    recommendation_params = Map.put(recommendation_params, "user_id", user.id)
    changeset = Recommendation.changeset(%Recommendation{}, recommendation_params)

    Repo.insert(changeset)

    conn
    |> put_flash(:info, "Your recommendation has been added.")
    |> redirect_to_back
  end

  def delete(conn, %{"id" => id}, user) do
    Repo.one(from(r in Recommendation, where: r.id == ^id and r.user_id == ^user.id))
    |> Repo.delete!()

    conn
    |> put_flash(:info, "Recommendation deleted successfully.")
    |> redirect(to: recommendation_frontend_path(conn, :my_recommendations))
  end

  def delete_all(conn, _, user) do
    from(r in Recommendation, where: r.user_id == ^user.id)
    |> Repo.delete_all()

    conn
    |> put_flash(:info, "All recommendations deleted successfully.")
    |> redirect(to: recommendation_frontend_path(conn, :my_recommendations))
  end

  defp redirect_to_back(conn) do
    path =
      conn
      |> Plug.Conn.get_req_header("referer")
      |> List.first()
      |> URI.parse()
      |> Map.get(:path)

    conn
    |> assign(:refer_path, path)
    |> redirect(to: path)
  end
end

defmodule PanWeb.RecommendationFrontendController do
  use PanWeb, :controller

  alias PanWeb.{Podcast, Recommendation, Subscription}

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

  def my_recommendations(conn, _params, user) do
    podcast_recommendations =
      Repo.all(
        from(r in Recommendation,
          where:
            r.user_id == ^user.id and
              not is_nil(r.podcast_id) and
              is_nil(r.episode_id) and
              is_nil(r.chapter_id),
          order_by: [desc: :inserted_at],
          preload: :podcast
        )
      )

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

    recommended_podcast_ids =
      Enum.map(podcast_recommendations, fn recommendation -> recommendation.podcast_id end)

    unrecommonded_podcast_ids =
      Enum.filter(subscribed_podcast_ids, fn id ->
        not Enum.member?(recommended_podcast_ids, id)
      end)

    unrecommended_podcasts =
      Repo.all(from(p in Podcast, where: p.id in ^unrecommonded_podcast_ids))

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "my_recommendations.html",
      podcast_recommendations: podcast_recommendations,
      unrecommended_podcasts: unrecommended_podcasts,
      changeset: changeset
    )
  end

  def create(conn, %{"recommendation" => recommendation_params}, user) do
    recommendation_params = Map.put(recommendation_params, "user_id", user.id)
    changeset = Recommendation.changeset(%Recommendation{}, recommendation_params)

    Repo.insert(changeset)

    conn
    |> put_flash(:info, "Your recommendation has been added.")
    |> redirect_to_back
  end

  def delete(conn, %{"id" => id}, user) do
    Repo.one(from(r in Recommendation, where: r.id == ^id and r.user_id == ^user.id))
    |> Repo.delete!()

    conn
    |> put_flash(:info, "Recommendation deleted successfully.")
    |> redirect(to: user_frontend_path(conn, :my_data))
  end

  def delete_all(conn, _, user) do
    from(r in Recommendation, where: r.user_id == ^user.id)
    |> Repo.delete_all()

    conn
    |> put_flash(:info, "All recommendations deleted successfully.")
    |> redirect(to: recommendation_frontend_path(conn, :my_recommendations))
  end

  defp redirect_to_back(conn) do
    path =
      conn
      |> Plug.Conn.get_req_header("referer")
      |> List.first()
      |> URI.parse()
      |> Map.get(:path)

    conn
    |> assign(:refer_path, path)
    |> redirect(to: path)
  end
end


Add regression test for head to head collisions

previous to the last commit when two snakes collidided in a head to head it
would copy the winning snake into the dead snake list, while still also having
it in the living snake array.

Although the problem is now fixed, this regression should test that behaviour.

defmodule BattleSnake.WorldTest do
  alias BattleSnake.{
    Point,
    Snake,
    World,
  }
  use BattleSnake.Case, async: true
  use Property
  use Point
  import Point

  setup context do
    world = %World{
      max_food: 4,
      height: 10,
      width: 10,
      game_id: 0,
    }
    Map.put context, :world, world
  end

  describe "World.dec_health_points/1" do
    setup do
      world = build(:world)
      snake = build(:snake, health_points: 50)

      [snake: _, world: world] =
        with_snake_in_world(snake: snake, world: world, length: 1)

      world = World.dec_health_points(world)

      [snake] = world.snakes

      {:ok,
       snake: snake,
       world: world}
    end

    test "reduces snake health points by 1", %{snake: snake} do
      assert snake.health_points == 49
    end
  end

  describe "World.grow_snakes/1" do
    setup do
      world = build(:world)
      snake = build(:snake, health_points: 50)

      [snake: snake, world: world] =
        with_snake_in_world(snake: snake, world: world, length: 1)

      world = with_food_on_snake(world: world, snake: snake)
      world = World.grow_snakes(world)
      [snake] = world.snakes

      {:ok,
       snake: snake,
       world: world}
    end

    test "resets the health_points of snakes that are eating this turn", %{snake: snake} do
      assert snake.health_points == 100
    end

    test "increases snake length", %{snake: snake} do
      assert length(snake.coords) == 2
    end
  end

  describe "#rand_unoccupied_space" do
    property "in the bounds of the board, on an unoccupied space" do
      forall world(), fn w ->
        point = World.rand_unoccupied_space(w)

        coords = Enum.flat_map w.snakes, &(&1.coords)

        ( not point in w.food and
          not point in coords and
          point.x in World.cols(w) and
          point.y in World.rows(w))
      end
    end
  end

  describe "#stock_food" do
    test "sets food on the board, up to a max", %{world: world} do
      world = World.stock_food(world)
      assert length(world.food) == 4
    end

    test "sets no food if set to 0", %{world: world} do
      world = put_in world.max_food, 0
      world = World.stock_food(world)
      assert length(world.food) == 0
    end

    test "adds to existing stocks", %{world: world} do
      food = %Point{y: 5, x: 5}
      world = put_in world.food, [food]
      world = put_in world.max_food, 2

      world = World.stock_food(world)

      assert([
        %Point{},
        ^food
      ] = world.food)
    end
  end

  describe "World.clean_up_dead/1 head to head collision" do
    test "kills same length snakes" do
      big_snake = (build :snake, id: :big, coords: line(p(0,0), p(0, 1), 3))
      small_snake = (build :snake, id: :small, coords: line(p(0,0), p(1, 0), 3))
      world = build(:world, width: 100, height: 100, snakes: [big_snake, small_snake])

      world = World.clean_up_dead(world)

      assert world.snakes == []
      assert small_snake in world.dead_snakes
      assert big_snake in world.dead_snakes
      assert 2 == length world.dead_snakes
    end

    test "kills the smaller snake" do
      big_snake = (build :snake, id: :big, coords: line(p(0,0), p(0, 1), 10))
      small_snake = (build :snake, id: :small, coords: line(p(0,0), p(1, 0), 3))
      world = build(:world, width: 100, height: 100, snakes: [big_snake, small_snake])

      world = World.clean_up_dead(world)

      assert world.snakes == [big_snake]
      assert world.dead_snakes == [small_snake]
    end
  end

  describe "World.clean_up_dead/1" do
    test "removes snakes that died in body collisions", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}, %Point{y: 5, x: 5}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    test "removes any snakes that die this turn", %{world: world} do
      snake = %Snake{coords: [%Point{y: 10, x: 10}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    @dead_snake %Snake{name: "dead"}
    @snake %Snake{name: "live", coords: [p(-1, 0)]}
    @world %World{turn: 10,
                  snakes: [@snake],
                  dead_snakes: [@dead_snake]}
    test "adds dead snakes to a list of deaths with the turn they died on" do
      world = World.clean_up_dead(@world)
      assert world.dead_snakes == [@dead_snake, @snake]
      assert world.snakes == []

      assert world.deaths == [
        %World.DeathEvent{turn: 10, snake: @snake}]
    end
  end

  describe "Poison.Encoder.encode(%BattleSnake.World{}, [me: snake])" do
    use Point

    @me %Snake{
      name: "me",
      url: "me.example.com",
      id: "1",
      coords: [
        p(1, 1),
      ]
    }

    @other %Snake{
      name: "other",
      url: "example.com",
      id: "2",
      coords: [
        p(0, 0),
        p(1, 0),
      ]
    }

    @world %World{
      turn: 0,
      height: 2,
      width: 2,
      food: [
        p(0, 1)
      ],
      snakes: [
        @me,
        @other,
      ],
      game_id: 0
    }

    @json %{
      "width" => 2,
      "height" => 2,
      "turn" => 0,
      "food" => [
        [0, 1]
      ],
      "you" => "1",
      "dead_snakes" => [],
      "snakes" => [
        %{
          "id" => "1",
          "taunt" => "",
          "name" => "me",
          "health_points" => 100,
          "coords" => [
            [1,1],
          ]
        },
        %{
          "id" => "2",
          "taunt" => "",
          "health_points" => 100,
          "name" => "other",
          "coords" => [
            [0,0],
            [1,0]
          ]
        }
      ],
      "game_id" => 0
    }

    @expected Poison.decode! Poison.encode!(@world, me: @me.id)

    test "formats as JSON" do
      assert @expected == @json
    end
  end

  describe "Poison.Encoder.encode(%BattleSnake.World{}, [])" do
    use Point

    @world %World{
      turn: 0,
      height: 2,
      width: 2,
      food: [
        p(0, 1)
      ],
      snakes: [
        %Snake{
          name: "me",
          id: "1",
          url: "me.example.com",
          coords: [
            p(1, 1),
          ]
        },
        %Snake{
          name: "other",
          id: "2",
          url: "example.com",
          coords: [
            p(0, 0),
            p(1, 0),
          ]
        }
      ],
      game_id: 0
    }

    @json %{
      "width" => 2,
      "height" => 2,
      "turn" => 0,
      "food" => [
        [0, 1]
      ],
      "dead_snakes" => [],
      "snakes" => [
        %{
          "id" => "1",
          "taunt" => "",
          "name" => "me",
          "health_points" => 100,
          "coords" => [
            [1,1],
          ]
        },
        %{
          "id" => "2",
          "taunt" => "",
          "health_points" => 100,
          "name" => "other",
          "coords" => [
            [0,0],
            [1,0]
          ]
        }
      ],
      "game_id" => 0
    }

    @expected PoisonTesting.cast! @world

    test "formats as JSON" do
      assert @expected == @json
    end
  end
end

defmodule BattleSnake.WorldTest do
  alias BattleSnake.{
    Point,
    Snake,
    World,
  }
  use BattleSnake.Case, async: true
  use Property
  use BattleSnake.Point

  setup context do
    world = %World{
      max_food: 4,
      height: 10,
      width: 10,
      game_id: 0,
    }
    Map.put context, :world, world
  end

  describe "World.dec_health_points/1" do
    setup do
      world = build(:world)
      snake = build(:snake, health_points: 50)

      [snake: _, world: world] =
        with_snake_in_world(snake: snake, world: world, length: 1)

      world = World.dec_health_points(world)

      [snake] = world.snakes

      {:ok,
       snake: snake,
       world: world}
    end

    test "reduces snake health points by 1", %{snake: snake} do
      assert snake.health_points == 49
    end
  end

  describe "World.grow_snakes/1" do
    setup do
      world = build(:world)
      snake = build(:snake, health_points: 50)

      [snake: snake, world: world] =
        with_snake_in_world(snake: snake, world: world, length: 1)

      world = with_food_on_snake(world: world, snake: snake)
      world = World.grow_snakes(world)
      [snake] = world.snakes

      {:ok,
       snake: snake,
       world: world}
    end

    test "resets the health_points of snakes that are eating this turn", %{snake: snake} do
      assert snake.health_points == 100
    end

    test "increases snake length", %{snake: snake} do
      assert length(snake.coords) == 2
    end
  end

  describe "#rand_unoccupied_space" do
    property "in the bounds of the board, on an unoccupied space" do
      forall world(), fn w ->
        point = World.rand_unoccupied_space(w)

        coords = Enum.flat_map w.snakes, &(&1.coords)

        ( not point in w.food and
          not point in coords and
          point.x in World.cols(w) and
          point.y in World.rows(w))
      end
    end
  end

  describe "#stock_food" do
    test "sets food on the board, up to a max", %{world: world} do
      world = World.stock_food(world)
      assert length(world.food) == 4
    end

    test "sets no food if set to 0", %{world: world} do
      world = put_in world.max_food, 0
      world = World.stock_food(world)
      assert length(world.food) == 0
    end

    test "adds to existing stocks", %{world: world} do
      food = %Point{y: 5, x: 5}
      world = put_in world.food, [food]
      world = put_in world.max_food, 2

      world = World.stock_food(world)

      assert([
        %Point{},
        ^food
      ] = world.food)
    end
  end

  describe "#clean_up_dead" do
    test "removes any snakes that die in head to heads", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}]}

      world = put_in world.snakes, [snake, snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake, snake]
    end

    test "removes snakes that died in body collisions", %{world: world} do
      snake = %Snake{coords: [%Point{y: 5, x: 5}, %Point{y: 5, x: 5}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    test "removes any snakes that die this turn", %{world: world} do
      snake = %Snake{coords: [%Point{y: 10, x: 10}]}
      world = put_in world.snakes, [snake]

      world = World.clean_up_dead(world)
      assert world.snakes == []
      assert world.dead_snakes == [snake]
    end

    @dead_snake %Snake{name: "dead"}
    @snake %Snake{name: "live", coords: [p(-1, 0)]}
    @world %World{turn: 10,
                  snakes: [@snake],
                  dead_snakes: [@dead_snake]}
    test "adds dead snakes to a list of deaths with the turn they died on" do
      world = World.clean_up_dead(@world)
      assert world.dead_snakes == [@dead_snake, @snake]
      assert world.snakes == []

      assert world.deaths == [
        %World.DeathEvent{turn: 10, snake: @snake}]
    end
  end

  describe "Poison.Encoder.encode(%BattleSnake.World{}, [me: snake])" do
    use Point

    @me %Snake{
      name: "me",
      url: "me.example.com",
      id: "1",
      coords: [
        p(1, 1),
      ]
    }

    @other %Snake{
      name: "other",
      url: "example.com",
      id: "2",
      coords: [
        p(0, 0),
        p(1, 0),
      ]
    }

    @world %World{
      turn: 0,
      height: 2,
      width: 2,
      food: [
        p(0, 1)
      ],
      snakes: [
        @me,
        @other,
      ],
      game_id: 0
    }

    @json %{
      "width" => 2,
      "height" => 2,
      "turn" => 0,
      "food" => [
        [0, 1]
      ],
      "you" => "1",
      "dead_snakes" => [],
      "snakes" => [
        %{
          "id" => "1",
          "taunt" => "",
          "name" => "me",
          "health_points" => 100,
          "coords" => [
            [1,1],
          ]
        },
        %{
          "id" => "2",
          "taunt" => "",
          "health_points" => 100,
          "name" => "other",
          "coords" => [
            [0,0],
            [1,0]
          ]
        }
      ],
      "game_id" => 0
    }

    @expected Poison.decode! Poison.encode!(@world, me: @me.id)

    test "formats as JSON" do
      assert @expected == @json
    end
  end

  describe "Poison.Encoder.encode(%BattleSnake.World{}, [])" do
    use Point

    @world %World{
      turn: 0,
      height: 2,
      width: 2,
      food: [
        p(0, 1)
      ],
      snakes: [
        %Snake{
          name: "me",
          id: "1",
          url: "me.example.com",
          coords: [
            p(1, 1),
          ]
        },
        %Snake{
          name: "other",
          id: "2",
          url: "example.com",
          coords: [
            p(0, 0),
            p(1, 0),
          ]
        }
      ],
      game_id: 0
    }

    @json %{
      "width" => 2,
      "height" => 2,
      "turn" => 0,
      "food" => [
        [0, 1]
      ],
      "dead_snakes" => [],
      "snakes" => [
        %{
          "id" => "1",
          "taunt" => "",
          "name" => "me",
          "health_points" => 100,
          "coords" => [
            [1,1],
          ]
        },
        %{
          "id" => "2",
          "taunt" => "",
          "health_points" => 100,
          "name" => "other",
          "coords" => [
            [0,0],
            [1,0]
          ]
        }
      ],
      "game_id" => 0
    }

    @expected PoisonTesting.cast! @world

    test "formats as JSON" do
      assert @expected == @json
    end
  end
end


Fix errant specs

Fix errant specs

defmodule Nerves.Artifact.BuildRunners.Docker.Volume do
  @moduledoc false
  import Nerves.Artifact.BuildRunners.Docker.Utils
  alias Nerves.Artifact

  @spec name(Nerves.Package.t()) :: String.t()
  def name(pkg) do
    "#{pkg.app}-#{id(pkg)}"
  end

  @spec id(Nerves.Package.t()) :: String.t()
  def id(pkg) do
    id_file = id_file(pkg)

    if File.exists?(id_file) do
      File.read!(id_file)
    else
      create_id(pkg)
      id(pkg)
    end
  end

  def id_file(pkg) do
    Artifact.build_path(pkg)
    |> Path.join(".docker_id")
  end

  def create_id(pkg) do
    id_file = id_file(pkg)
    id = Nerves.Utils.random_alpha_num(16)

    Path.dirname(id_file)
    |> File.mkdir_p!()

    File.write!(id_file, id)
  end

  def delete(volume_name) do
    shell_info("Deleting build volume #{volume_name}")
    args = ["volume", "rm", volume_name]

    case Mix.Nerves.Utils.shell("docker", args) do
      {_result, 0} ->
        :ok

      {_result, _} ->
        Mix.raise("""
        Nerves Docker build_runner encountered an error while deleting volume #{volume_name}
        """)
    end
  end

  def exists?(volume_name) do
    cmd = "docker"
    args = ["volume", "ls", "-f", "name=#{volume_name}", "-q"]

    case Nerves.Port.cmd(cmd, args, stderr_to_stdout: true) do
      {<<^volume_name, _tail::binary>>, 0} ->
        true

      {<<"Cannot connect to the Docker daemon", _tail::binary>>, _} ->
        Mix.raise("Nerves Docker build_runner is unable to connect to docker daemon")

      _ ->
        false
    end
  end

  def create(volume_name) do
    cmd = "docker"
    args = ["volume", "create", "--name", volume_name]

    case Nerves.Port.cmd(cmd, args) do
      {_, 0} -> :noop
      _ -> Mix.raise("Nerves Docker build_runner could not create docker volume #{volume_name}")
    end
  end
end

defmodule Nerves.Artifact.BuildRunners.Docker.Volume do
  @moduledoc false
  import Nerves.Artifact.BuildRunners.Docker.Utils
  alias Nerves.Artifact

  @spec name(Package.t()) :: String.t()
  def name(pkg) do
    "#{pkg.app}-#{id(pkg)}"
  end

  @spec id(Package.t()) :: String.t()
  def id(pkg) do
    id_file = id_file(pkg)

    if File.exists?(id_file) do
      File.read!(id_file)
    else
      create_id(pkg)
      id(pkg)
    end
  end

  def id_file(pkg) do
    Artifact.build_path(pkg)
    |> Path.join(".docker_id")
  end

  def create_id(pkg) do
    id_file = id_file(pkg)
    id = Nerves.Utils.random_alpha_num(16)

    Path.dirname(id_file)
    |> File.mkdir_p!()

    File.write!(id_file, id)
  end

  def delete(volume_name) do
    shell_info("Deleting build volume #{volume_name}")
    args = ["volume", "rm", volume_name]

    case Mix.Nerves.Utils.shell("docker", args) do
      {_result, 0} ->
        :ok

      {_result, _} ->
        Mix.raise("""
        Nerves Docker build_runner encountered an error while deleting volume #{volume_name}
        """)
    end
  end

  def exists?(volume_name) do
    cmd = "docker"
    args = ["volume", "ls", "-f", "name=#{volume_name}", "-q"]

    case Nerves.Port.cmd(cmd, args, stderr_to_stdout: true) do
      {<<^volume_name, _tail::binary>>, 0} ->
        true

      {<<"Cannot connect to the Docker daemon", _tail::binary>>, _} ->
        Mix.raise("Nerves Docker build_runner is unable to connect to docker daemon")

      _ ->
        false
    end
  end

  def create(volume_name) do
    cmd = "docker"
    args = ["volume", "create", "--name", volume_name]

    case Nerves.Port.cmd(cmd, args) do
      {_, 0} -> :noop
      _ -> Mix.raise("Nerves Docker build_runner could not create docker volume #{volume_name}")
    end
  end
end


Make `recent` transactions as 30 days ago

defmodule ExMoney.Transaction do
  use ExMoney.Web, :model

  alias ExMoney.Transaction

  schema "transactions" do
    field :saltedge_transaction_id, :integer
    field :mode, :string
    field :status, :string
    field :made_on, Ecto.Date
    field :amount, :decimal
    field :currency_code, :string
    field :description, :string
    field :duplicated, :boolean, default: false

    has_one :transaction_info, ExMoney.TransactionInfo
    belongs_to :category, ExMoney.Category
    belongs_to :user, ExMoney.User
    belongs_to :account, ExMoney.Account
    belongs_to :saltedge_account, ExMoney.Account,
      foreign_key: :saltedge_account_id,
      references: :saltedge_account_id

    timestamps
  end

  @required_fields ~w(
    saltedge_transaction_id
    mode
    status
    made_on
    amount
    currency_code
    description
    duplicated
    saltedge_account_id
    user_id
  )
  @optional_fields ~w(category_id)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

  def changeset_custom(model, params \\ :empty) do
    model
    |> cast(params, ~w(description amount category_id account_id made_on user_id), ~w())
  end

  def by_user_id(user_id) do
    from tr in Transaction,
      where: tr.user_id == ^user_id
  end

  def by_saltedge_transaction_id(transaction_id) do
    from tr in Transaction,
      where: tr.saltedge_transaction_id == ^transaction_id,
      limit: 1
  end

  def recent() do
    current_date = Timex.Date.local
    from = Timex.Date.shift(current_date, days: -30)

    from tr in Transaction,
      where: tr.made_on >= ^from,
      preload: [:transaction_info, :category],
      order_by: [desc: tr.made_on]
  end

  def last_month() do
    current_date = Timex.Date.local
    from = first_day_of_month(current_date)
    to = last_day_of_month(current_date)

    from tr in Transaction,
      where: tr.made_on >= ^from,
      where: tr.made_on <= ^to
  end

  # FIXME cache instead of db
  def newest(saltedge_account_id) do
    from tr in Transaction,
      where: tr.saltedge_account_id == ^saltedge_account_id,
      order_by: [desc: tr.made_on],
      limit: 1
  end

  def newest do
    from tr in Transaction,
      order_by: [desc: tr.made_on],
      limit: 1
  end

  defp first_day_of_month(date) do
    Timex.Date.from({{date.year, date.month, 0}, {0, 0, 0}})
    |> Timex.DateFormat.format("%Y-%m-%d", :strftime)
    |> elem(1)
  end

  defp last_day_of_month(date) do
    days_in_month = Timex.Date.days_in_month(date)

    Timex.Date.from({{date.year, date.month, days_in_month}, {23, 59, 59}})
    |> Timex.DateFormat.format("%Y-%m-%d", :strftime)
    |> elem(1)
  end
end

defmodule ExMoney.Transaction do
  use ExMoney.Web, :model

  alias ExMoney.Transaction

  schema "transactions" do
    field :saltedge_transaction_id, :integer
    field :mode, :string
    field :status, :string
    field :made_on, Ecto.Date
    field :amount, :decimal
    field :currency_code, :string
    field :description, :string
    field :duplicated, :boolean, default: false

    has_one :transaction_info, ExMoney.TransactionInfo
    belongs_to :category, ExMoney.Category
    belongs_to :user, ExMoney.User
    belongs_to :account, ExMoney.Account
    belongs_to :saltedge_account, ExMoney.Account,
      foreign_key: :saltedge_account_id,
      references: :saltedge_account_id

    timestamps
  end

  @required_fields ~w(
    saltedge_transaction_id
    mode
    status
    made_on
    amount
    currency_code
    description
    duplicated
    saltedge_account_id
    user_id
  )
  @optional_fields ~w(category_id)

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
  end

  def changeset_custom(model, params \\ :empty) do
    model
    |> cast(params, ~w(description amount category_id account_id made_on user_id), ~w())
  end

  def by_user_id(user_id) do
    from tr in Transaction,
      where: tr.user_id == ^user_id
  end

  def by_saltedge_transaction_id(transaction_id) do
    from tr in Transaction,
      where: tr.saltedge_transaction_id == ^transaction_id,
      limit: 1
  end

  def recent() do
    current_date = Timex.Date.local
    from = first_day_of_month(current_date)

    from tr in Transaction,
      where: tr.made_on >= ^from,
      preload: [:transaction_info, :category],
      order_by: [desc: tr.made_on]
  end

  def last_month() do
    current_date = Timex.Date.local
    from = first_day_of_month(current_date)
    to = last_day_of_month(current_date)

    from tr in Transaction,
      where: tr.made_on >= ^from,
      where: tr.made_on <= ^to
  end

  # FIXME cache instead of db
  def newest(saltedge_account_id) do
    from tr in Transaction,
      where: tr.saltedge_account_id == ^saltedge_account_id,
      order_by: [desc: tr.made_on],
      limit: 1
  end

  def newest do
    from tr in Transaction,
      order_by: [desc: tr.made_on],
      limit: 1
  end

  defp first_day_of_month(date) do
    Timex.Date.from({{date.year, date.month, 0}, {0, 0, 0}})
    |> Timex.DateFormat.format("%Y-%m-%d", :strftime)
    |> elem(1)
  end

  defp last_day_of_month(date) do
    days_in_month = Timex.Date.days_in_month(date)

    Timex.Date.from({{date.year, date.month, days_in_month}, {23, 59, 59}})
    |> Timex.DateFormat.format("%Y-%m-%d", :strftime)
    |> elem(1)
  end
end


cowboy/elixir: This if-construct is almost identically ported from its Pythonic equivalent ! :smiley:

#
# src/elixir/lib/dnsresolvd.ex
# =============================================================================
# DNS Resolver Daemon (dnsresolvd). Version 0.1
# =============================================================================
# A daemon that performs DNS lookups for the given hostname
# passed in an HTTP request, with the focus on its implementation
# using various programming languages. (Cowboy-boosted impl.)
# =============================================================================
# Copyright (C) 2017-2018 Radislav (Radicchio) Golubtsov
#
# (See the LICENSE file at the top of the source tree.)
#

defmodule DnsResolvd do
    @moduledoc "The main Application module of the daemon."

    use Application

    @doc """
    Starts up the daemon.

    It has to be the application module callback, but used directly
    from the startup script of the daemon.

    **Args:**

        `args`: A list containing the server port number to listen on
                as the first element.

    **Returns:**

        The tuple containing the PID of the top supervisor process
        and the state of the running application (or an empty list).
    """
    @impl (true)
    def start(_, args) do
        [port_number | _] = args

        # Using this call instead of passing "--app dnsresolvd"
        # to the Elixir interpreter. See the file "dnsresolvd.app"
        # for explanation.
        {:ok, _} = Application.ensure_all_started(:cowboy)

        dispatch = :cowboy_router.compile([
            {:_, [
                {'/', ReqHandler, []}
            ]}
        ])

        # Starting up the plain HTTP listener on <port_number>.
        {:ok, _} = :cowboy.start_clear(:http_listener, [
            port: port_number
        ], %{
            env: %{:dispatch => dispatch}
        })

        # Starting up the daemon's provided Supervisor (optional).
        DnsResolvs.start_link()

        # Trapping exit signals, i.e. transforming them into {:EXIT} message.
        Process.flag(:trap_exit, true)

        # Inspecting the daemon's Application process message queue
        # for the incoming {:EXIT} message, i.e. doing same things
        # as the Process.sleep(:infinity) inactive call below
        # until the message received.
        receive do
            {:EXIT, from, reason} ->
                IO.inspect(from)
                IO.inspect(reason)
        end

        # --- Use message queue (as above) instead of sleep() call below ------
        # Using this call instead of passing "--no-halt"
        # to the Elixir interpreter. See the file "dnsresolvd.app"
        # for explanation.
        # Process.sleep(:infinity)
        # ---------------------------------------------------------------------
    end
end

defmodule ReqHandler do
    @moduledoc "The default HTTP request handler."

    @doc """
    The request handler `init/2` callback.

    Gets called when a new incoming HTTP request is received.

    **Args:**

        `req`:   The incoming HTTP request object.
        `state`: The initial state of the HTTP handler.

    **Returns:**

        The tuple containing the HTTP response to be rendered
        and a new state of the HTTP handler.
    """
    def init(req, state) do
        mtd = :cowboy_req.method(req)

        IO.puts(mtd)

        # ---------------------------------------------------------------------
        # --- Parsing and validating request params - Begin -------------------
        # ---------------------------------------------------------------------
        {:ok, params, req} = cond do
            (mtd === AUX._MTD_HTTP_GET ) ->
                {:ok, :cowboy_req.parse_qs(req), req}
            (mtd === AUX._MTD_HTTP_POST) ->
                :cowboy_req.read_urlencoded_body(req)
        end

        IO.inspect(params)

        for (i <- 0..(length(params) - 1)) do
            IO.inspect(Enum.at(params, i))
        end

        hostname = for (param <- params) do
            {k, v} = param; if (k === "h"), do: v # <---+-----------------+
        end #     +----GET----+-----+-----+             |                 |
        #         |     |     |     |     |       +-----+      +----------+-+
        #         v     v     v     v     v       |            |          | |
        # $ curl 'http://localhost:<port_number>/?h=<hostname>&f=<fmt>'   | |
        # $                                                               | |
        # $ curl -d 'h=<hostname>&f=<fmt>' http://localhost:<port_number> | |
        #         ^  |            |                                       | |
        #         |  +------------+---------------------------------------+ |
        #         |               |                                         |
        # POST----+               +---------------------+                   |
        #                                               |                   |
        fmt      = for (param <- params) do #           |                   |
            {k, v} = param; if (k === "f"), do: v # <---+-------------------+
        end

        hostname = hostname |> Enum.filter(fn(v) -> (v !== nil) end)
        hostname = hostname |> Enum.at(length(hostname) - 1)
                   hostname |> IO.inspect()

        fmt      = fmt      |> Enum.filter(fn(v) -> (v !== nil) end)
        fmt      = fmt      |> Enum.at(length(fmt     ) - 1)
                   fmt      |> IO.inspect()

        {hostname, fmt}

        hostname = if ((              hostname  === nil )
                   or  (              hostname  === true)
                   or  (String.length(hostname) === 0   )) do
            AUX._DEF_HOSTNAME
        else
            hostname
        end

        fmt      = if ((              fmt       === nil )
                   or  (              fmt       === true)
                   or  (String.length(fmt     ) === 0   )) do
            AUX._PRM_FMT_JSON
        else
            String.downcase(fmt, :ascii)
        end

        # This if-construct is almost identically ported
        # from its Pythonic equivalent ! :-)
        fmt = if (fmt not in [
            AUX._PRM_FMT_HTML,
            AUX._PRM_FMT_JSON,
        ]) do
            AUX._PRM_FMT_JSON
        else
            fmt
        end

        IO.puts(hostname)
        IO.puts(fmt     )
        # ---------------------------------------------------------------------
        # --- Parsing and validating request params - End ---------------------
        # ---------------------------------------------------------------------

        {:ok,
            req,  # <== For the moment the response is the same as the request.
            state # <== The state of the handler doesn't need to be changed.
        }
    end

    ###
    # Performs DNS lookup action for the given hostname,
    # i.e. (in this case) IP address retrieval by hostname.
    #
    # Args:
    #     hostname: The effective hostname to look up for.
    #
    # Returns:
    #     The array containing IP address of the analyzing host/service
    #     and corresponding IP version (family) used to look up in DNS:
    #     "4" for IPv4-only hosts, "6" for IPv6-capable hosts.
    #
    defp dns_lookup(hostname) do
        # TODO: Implement performing DNS lookup action for the given hostname.
    end
end

defmodule DnsResolvs do
    @moduledoc "The main Supervisor module of the daemon."

    use Supervisor

    @doc """
    Supervisor startup helper.

    Used to directly start up the `DnsResolvs` supervisor process.

    **Returns:**

        The PID of the `DnsResolvs` supervisor process.
    """
    def start_link() do
        Supervisor.start_link(__MODULE__, [])
    end

    @doc """
    Supervisor `init/1` callback.

    Gets called when the `DnsResolvs` supervisor is about to be started up.

    **Returns:**

        The tuple containing supervisor flags
        and child processes' specifications.
    """
    @impl (true)
    def init(_) do
        child_procs = [] # <== No child processes do we need.

        Supervisor.init(child_procs, strategy: :one_for_one)
    end
end

# vim:set nu ts=4 sw=4:

#
# src/elixir/lib/dnsresolvd.ex
# =============================================================================
# DNS Resolver Daemon (dnsresolvd). Version 0.1
# =============================================================================
# A daemon that performs DNS lookups for the given hostname
# passed in an HTTP request, with the focus on its implementation
# using various programming languages. (Cowboy-boosted impl.)
# =============================================================================
# Copyright (C) 2017-2018 Radislav (Radicchio) Golubtsov
#
# (See the LICENSE file at the top of the source tree.)
#

defmodule DnsResolvd do
    @moduledoc "The main Application module of the daemon."

    use Application

    @doc """
    Starts up the daemon.

    It has to be the application module callback, but used directly
    from the startup script of the daemon.

    **Args:**

        `args`: A list containing the server port number to listen on
                as the first element.

    **Returns:**

        The tuple containing the PID of the top supervisor process
        and the state of the running application (or an empty list).
    """
    @impl (true)
    def start(_, args) do
        [port_number | _] = args

        # Using this call instead of passing "--app dnsresolvd"
        # to the Elixir interpreter. See the file "dnsresolvd.app"
        # for explanation.
        {:ok, _} = Application.ensure_all_started(:cowboy)

        dispatch = :cowboy_router.compile([
            {:_, [
                {'/', ReqHandler, []}
            ]}
        ])

        # Starting up the plain HTTP listener on <port_number>.
        {:ok, _} = :cowboy.start_clear(:http_listener, [
            port: port_number
        ], %{
            env: %{:dispatch => dispatch}
        })

        # Starting up the daemon's provided Supervisor (optional).
        DnsResolvs.start_link()

        # Trapping exit signals, i.e. transforming them into {:EXIT} message.
        Process.flag(:trap_exit, true)

        # Inspecting the daemon's Application process message queue
        # for the incoming {:EXIT} message, i.e. doing same things
        # as the Process.sleep(:infinity) inactive call below
        # until the message received.
        receive do
            {:EXIT, from, reason} ->
                IO.inspect(from)
                IO.inspect(reason)
        end

        # --- Use message queue (as above) instead of sleep() call below ------
        # Using this call instead of passing "--no-halt"
        # to the Elixir interpreter. See the file "dnsresolvd.app"
        # for explanation.
        # Process.sleep(:infinity)
        # ---------------------------------------------------------------------
    end
end

defmodule ReqHandler do
    @moduledoc "The default HTTP request handler."

    @doc """
    The request handler `init/2` callback.

    Gets called when a new incoming HTTP request is received.

    **Args:**

        `req`:   The incoming HTTP request object.
        `state`: The initial state of the HTTP handler.

    **Returns:**

        The tuple containing the HTTP response to be rendered
        and a new state of the HTTP handler.
    """
    def init(req, state) do
        mtd = :cowboy_req.method(req)

        IO.puts(mtd)

        # ---------------------------------------------------------------------
        # --- Parsing and validating request params - Begin -------------------
        # ---------------------------------------------------------------------
        {:ok, params, req} = cond do
            (mtd === AUX._MTD_HTTP_GET ) ->
                {:ok, :cowboy_req.parse_qs(req), req}
            (mtd === AUX._MTD_HTTP_POST) ->
                :cowboy_req.read_urlencoded_body(req)
        end

        IO.inspect(params)

        for (i <- 0..(length(params) - 1)) do
            IO.inspect(Enum.at(params, i))
        end

        hostname = for (param <- params) do
            {k, v} = param; if (k === "h"), do: v # <---+-----------------+
        end #     +----GET----+-----+-----+             |                 |
        #         |     |     |     |     |       +-----+      +----------+-+
        #         v     v     v     v     v       |            |          | |
        # $ curl 'http://localhost:<port_number>/?h=<hostname>&f=<fmt>'   | |
        # $                                                               | |
        # $ curl -d 'h=<hostname>&f=<fmt>' http://localhost:<port_number> | |
        #         ^  |            |                                       | |
        #         |  +------------+---------------------------------------+ |
        #         |               |                                         |
        # POST----+               +---------------------+                   |
        #                                               |                   |
        fmt      = for (param <- params) do #           |                   |
            {k, v} = param; if (k === "f"), do: v # <---+-------------------+
        end

        hostname = hostname |> Enum.filter(fn(v) -> (v !== nil) end)
        hostname = hostname |> Enum.at(length(hostname) - 1)
                   hostname |> IO.inspect()

        fmt      = fmt      |> Enum.filter(fn(v) -> (v !== nil) end)
        fmt      = fmt      |> Enum.at(length(fmt     ) - 1)
                   fmt      |> IO.inspect()

        {hostname, fmt}

        hostname = if ((              hostname  === nil )
                   or  (              hostname  === true)
                   or  (String.length(hostname) === 0   )) do
            AUX._DEF_HOSTNAME
        else
            hostname
        end

        fmt      = if ((              fmt       === nil )
                   or  (              fmt       === true)
                   or  (String.length(fmt     ) === 0   )) do
            AUX._PRM_FMT_JSON
        else
            String.downcase(fmt, :ascii)
        end

        IO.puts(hostname)
        IO.puts(fmt     )
        # ---------------------------------------------------------------------
        # --- Parsing and validating request params - End ---------------------
        # ---------------------------------------------------------------------

        {:ok,
            req,  # <== For the moment the response is the same as the request.
            state # <== The state of the handler doesn't need to be changed.
        }
    end

    ###
    # Performs DNS lookup action for the given hostname,
    # i.e. (in this case) IP address retrieval by hostname.
    #
    # Args:
    #     hostname: The effective hostname to look up for.
    #
    # Returns:
    #     The array containing IP address of the analyzing host/service
    #     and corresponding IP version (family) used to look up in DNS:
    #     "4" for IPv4-only hosts, "6" for IPv6-capable hosts.
    #
    defp dns_lookup(hostname) do
        # TODO: Implement performing DNS lookup action for the given hostname.
    end
end

defmodule DnsResolvs do
    @moduledoc "The main Supervisor module of the daemon."

    use Supervisor

    @doc """
    Supervisor startup helper.

    Used to directly start up the `DnsResolvs` supervisor process.

    **Returns:**

        The PID of the `DnsResolvs` supervisor process.
    """
    def start_link() do
        Supervisor.start_link(__MODULE__, [])
    end

    @doc """
    Supervisor `init/1` callback.

    Gets called when the `DnsResolvs` supervisor is about to be started up.

    **Returns:**

        The tuple containing supervisor flags
        and child processes' specifications.
    """
    @impl (true)
    def init(_) do
        child_procs = [] # <== No child processes do we need.

        Supervisor.init(child_procs, strategy: :one_for_one)
    end
end

# vim:set nu ts=4 sw=4:


Fix new cs node docs

Fix new cs node docs

defmodule Mix.Tasks.Farmbot.Gen.CeleryScript.Node do
  @moduledoc "Generate a CeleryScript Node."
  use Mix.Task

  @usage """
  usage:
    mix farmbot.gen.celery_script.node SomeCamelModuleName --args comma,seperated,list,of,args
  """

  @shortdoc @moduledoc
  @code_template_path "lib/mix/tasks/farmbot/gen/celery_script/node.ex.eex"
  @test_template_path "lib/mix/tasks/farmbot/gen/celery_script/node_test.ex.eex"

  @node_path "lib/farmbot/celery_script/ast/node/"
  @test_path "test/farmbot/celery_script/ast/node/"

  def run([]) do
    Mix.raise @usage
  end

  def run(args) do
    case OptionParser.parse(args) do
      {opts, [module], _} ->
        namespace = Module.split(Farmbot.CeleryScript.AST.Node)
        full_mod = [namespace | [module]] |> List.flatten |> Module.concat()
        allow_args = check_args(opts)
        code = EEx.eval_file(@code_template_path, [module: full_mod |> to_string() |> String.trim_leading("Elixir."), allow_args: allow_args])
        test = EEx.eval_file(@test_template_path, [module: full_mod |> to_string() |> String.trim_leading("Elixir."), alias_module: module])
        do_write_files(module, full_mod, code, test)
      _ ->
        Mix.raise @usage
    end
  end

  defp check_args(opts) do
    cs_args = Keyword.get(opts, :args, [])
    arg_list = String.split(cs_args, ",")
    allow_args = Enum.map(arg_list, fn(arg) ->
      a = Macro.camelize(arg)
      namespace = Module.split(Farmbot.CeleryScript.AST.Arg)
      full_mod = [namespace | [a]] |> List.flatten |> Module.concat()
      unless Code.ensure_loaded?(full_mod) do
        Mix.raise "Unknown arg: #{arg} (#{full_mod})"
      end
      String.to_atom(arg)
    end)

    allow_args
  end

  defp do_write_files(module, full_mod, code, test) do
    case Code.eval_string(code) do
      {{:module, ^full_mod, _, _}, _} ->
        code_file_name = Macro.underscore(module) <> ".ex"
        code_file_path = Path.join(@node_path, code_file_name)
        if File.exists?(code_file_path) do
          Mix.raise "#{code_file_path} already exists."
        end
        File.write!(code_file_path, code)

        test_file_name = Macro.underscore(module) <> "_test.exs"
        test_file_path = Path.join(@test_path, test_file_name)
        if File.exists?(test_file_path) do
          Mix.raise "#{test_file_path} already exists."
        end
        File.write!(test_file_path, test)
        Mix.shell.info [:green, "New node; #{code_file_path}"]

      _ -> Mix.raise "Invalid module: #{module}"
    end
  end

end

defmodule Mix.Tasks.Farmbot.Gen.CeleryScript.Node do
  @moduledoc "Generate a CeleryScript Node."
  use Mix.Task

  @usage """
  usage:
    mix farmbot.celery_script.gen.node SomeCamelModuleName --args comma,seperated,list,of,args
  """

  @shortdoc @moduledoc
  @code_template_path "lib/mix/tasks/farmbot/gen/celery_script/node.ex.eex"
  @test_template_path "lib/mix/tasks/farmbot/gen/celery_script/node_test.ex.eex"

  @node_path "lib/farmbot/celery_script/ast/node/"
  @test_path "test/farmbot/celery_script/ast/node/"

  def run([]) do
    Mix.raise @usage
  end

  def run(args) do
    case OptionParser.parse(args) do
      {opts, [module], _} ->
        namespace = Module.split(Farmbot.CeleryScript.AST.Node)
        full_mod = [namespace | [module]] |> List.flatten |> Module.concat()
        allow_args = check_args(opts)
        code = EEx.eval_file(@code_template_path, [module: full_mod |> to_string() |> String.trim_leading("Elixir."), allow_args: allow_args])
        test = EEx.eval_file(@test_template_path, [module: full_mod |> to_string() |> String.trim_leading("Elixir."), alias_module: module])
        do_write_files(module, full_mod, code, test)
      _ ->
        Mix.raise @usage
    end
  end

  defp check_args(opts) do
    cs_args = Keyword.get(opts, :args, [])
    arg_list = String.split(cs_args, ",")
    allow_args = Enum.map(arg_list, fn(arg) ->
      a = Macro.camelize(arg)
      namespace = Module.split(Farmbot.CeleryScript.AST.Arg)
      full_mod = [namespace | [a]] |> List.flatten |> Module.concat()
      unless Code.ensure_loaded?(full_mod) do
        Mix.raise "Unknown arg: #{arg} (#{full_mod})"
      end
      String.to_atom(arg)
    end)

    allow_args
  end

  defp do_write_files(module, full_mod, code, test) do
    case Code.eval_string(code) do
      {{:module, ^full_mod, _, _}, _} ->
        code_file_name = Macro.underscore(module) <> ".ex"
        code_file_path = Path.join(@node_path, code_file_name)
        if File.exists?(code_file_path) do
          Mix.raise "#{code_file_path} already exists."
        end
        File.write!(code_file_path, code)

        test_file_name = Macro.underscore(module) <> "_test.exs"
        test_file_path = Path.join(@test_path, test_file_name)
        if File.exists?(test_file_path) do
          Mix.raise "#{test_file_path} already exists."
        end
        File.write!(test_file_path, test)
        Mix.shell.info [:green, "New node; #{code_file_path}"]

      _ -> Mix.raise "Invalid module: #{module}"
    end
  end

end


Use List.foldr in Console metadata handling

defmodule Logger.Backends.Console do
  @moduledoc false

  use GenEvent

  def init(:console) do
    if Process.whereis(:user) do
      init({:user, []})
    else
      {:error, :ignore}
    end
  end

  def init({device, opts}) do
    {:ok, configure(device, opts)}
  end

  def handle_call({:configure, options}, state) do
    {:ok, :ok, configure(state.device, options)}
  end

  def handle_event({_level, gl, _event}, state)
  when node(gl) != node() do
    {:ok, state}
  end

  def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do
    if meet_level?(level, state.level) do
      log_event(level, msg, ts, md, state)
    end
    {:ok, state}
  end

  ## Helpers

  defp meet_level?(_lvl, nil), do: true

  defp meet_level?(lvl, min) do
    Logger.compare_levels(lvl, min) != :lt
  end

  defp configure(device, options) do
    config =
      Application.get_env(:logger, :console, [])
      |> configure_merge(options)

    if device === :user do
      Application.put_env(:logger, :console, config)
    end

    format =
      Keyword.get(config, :format)
      |> Logger.Formatter.compile
    level    = Keyword.get(config, :level)
    metadata = Keyword.get(config, :metadata, [])
    colors   = configure_colors(config)
    %{format: format, metadata: metadata,
      level: level, colors: colors, device: device}
  end

  defp configure_merge(env, options) do
    Keyword.merge(env, options, fn
      :colors, v1, v2 -> Keyword.merge(v1, v2)
      _, _v1, v2 -> v2
    end)
  end

  defp configure_colors(config) do
    colors = Keyword.get(config, :colors, [])
    %{debug: Keyword.get(colors, :debug, :cyan),
      info: Keyword.get(colors, :info, :normal),
      warn: Keyword.get(colors, :warn, :yellow),
      error: Keyword.get(colors, :error, :red),
      enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?)}
  end

  defp log_event(level, msg, ts, md, %{colors: colors} = state) do
    output =
      format_event(level, msg, ts, md, state)
      |> color_event(level, colors)
    IO.write(state.device, output)
  end

  defp format_event(level, msg, ts, md, %{format: format, metadata: keys}) do
    Logger.Formatter.format(format, level, msg, ts, take_metadata(md, keys))
  end

  defp take_metadata(metadata, keys) do
    List.foldr keys, [], fn key, acc ->
      case Keyword.fetch(metadata, key) do
        {:ok, val} -> [{key, val} | acc]
        :error     -> acc
      end
    end
  end

  defp color_event(data, _level, %{enabled: false}), do: data

  defp color_event(data, level, %{enabled: true} = colors) do
    [IO.ANSI.format_fragment(Map.fetch!(colors, level), true), data | IO.ANSI.reset]
  end
end

defmodule Logger.Backends.Console do
  @moduledoc false

  use GenEvent

  def init(:console) do
    if Process.whereis(:user) do
      init({:user, []})
    else
      {:error, :ignore}
    end
  end

  def init({device, opts}) do
    {:ok, configure(device, opts)}
  end

  def handle_call({:configure, options}, state) do
    {:ok, :ok, configure(state.device, options)}
  end

  def handle_event({_level, gl, _event}, state)
  when node(gl) != node() do
    {:ok, state}
  end

  def handle_event({level, _gl, {Logger, msg, ts, md}}, state) do
    if meet_level?(level, state.level) do
      log_event(level, msg, ts, md, state)
    end
    {:ok, state}
  end

  ## Helpers

  defp meet_level?(_lvl, nil), do: true

  defp meet_level?(lvl, min) do
    Logger.compare_levels(lvl, min) != :lt
  end

  defp configure(device, options) do
    config =
      Application.get_env(:logger, :console, [])
      |> configure_merge(options)

    if device === :user do
      Application.put_env(:logger, :console, config)
    end

    format =
      Keyword.get(config, :format)
      |> Logger.Formatter.compile
    level    = Keyword.get(config, :level)
    metadata = Keyword.get(config, :metadata, [])
    colors   = configure_colors(config)
    %{format: format, metadata: metadata,
      level: level, colors: colors, device: device}
  end

  defp configure_merge(env, options) do
    Keyword.merge(env, options, fn
      :colors, v1, v2 -> Keyword.merge(v1, v2)
      _, _v1, v2 -> v2
    end)
  end

  defp configure_colors(config) do
    colors = Keyword.get(config, :colors, [])
    %{debug: Keyword.get(colors, :debug, :cyan),
      info: Keyword.get(colors, :info, :normal),
      warn: Keyword.get(colors, :warn, :yellow),
      error: Keyword.get(colors, :error, :red),
      enabled: Keyword.get(colors, :enabled, IO.ANSI.enabled?)}
  end

  defp log_event(level, msg, ts, md, %{colors: colors} = state) do
    output =
      format_event(level, msg, ts, md, state)
      |> color_event(level, colors)
    IO.write(state.device, output)
  end

  defp format_event(level, msg, ts, md, %{format: format, metadata: keys}) do
    Logger.Formatter.format(format, level, msg, ts, take_metadata(md, keys))
  end

  defp take_metadata(metadata, keys) do
    Enum.reduce(keys, [], fn key, acc ->
      case Keyword.fetch(metadata, key) do
        {:ok, val} -> [{key, val} | acc]
        :error     -> acc
      end
    end) |> Enum.reverse()
  end

  defp color_event(data, _level, %{enabled: false}), do: data

  defp color_event(data, level, %{enabled: true} = colors) do
    [IO.ANSI.format_fragment(Map.fetch!(colors, level), true), data | IO.ANSI.reset]
  end
end


Do not test System.pwd return value.

Do not test System.pwd return value.

The function relies on Erlang's filename:abs function, so we're
basically testing Erlang here. There is no straightforward way to obtain
current directory by other means that'll work seamlessly on all OSes.
So we'll better not check the return value.

Code.require_file "../test_helper", __FILE__

require Erlang.os, as: OS

defmodule System.ARGVTest do
  use ExUnit.Case

  test :argv do
    list = OS.cmd('bin/elixir -e "IO.inspect System.argv" -- -o opt arg1 arg2 --long-opt 10')
    { args, _ } = Code.eval list, []
    expected = ["-o", "opt", "arg1", "arg2", "--long-opt", "10"]
    assert_equal expected, args
  end
end

defmodule SystemTest do
  use ExUnit.Case

  test :at_exit do
    output = OS.cmd('bin/elixir -e "System.at_exit(fn(x) -> IO.inspect x end)"')
    assert_equal '0\n', output
  end

  test :env do
    assert_equal nil, System.get_env("SYSTEM_ENV_TEST_VAR")
    System.put_env('SYSTEM_ENV_TEST_VAR', 'SAMPLE')
    assert_equal "SAMPLE", System.get_env("SYSTEM_ENV_TEST_VAR")
  end

  test :cmd do
    assert is_binary(System.cmd "binary")
  end

  test :pwd do
    assert is_binary(System.pwd)
  end
end

Code.require_file "../test_helper", __FILE__

require Erlang.os, as: OS

defmodule System.ARGVTest do
  use ExUnit.Case

  test :argv do
    list = OS.cmd('bin/elixir -e "IO.inspect System.argv" -- -o opt arg1 arg2 --long-opt 10')
    { args, _ } = Code.eval list, []
    expected = ["-o", "opt", "arg1", "arg2", "--long-opt", "10"]
    assert_equal expected, args
  end
end

defmodule SystemTest do
  use ExUnit.Case

  test :at_exit do
    output = OS.cmd('bin/elixir -e "System.at_exit(fn(x) -> IO.inspect x end)"')
    assert_equal '0\n', output
  end

  test :env do
    assert_equal nil, System.get_env("SYSTEM_ENV_TEST_VAR")
    System.put_env('SYSTEM_ENV_TEST_VAR', 'SAMPLE')
    assert_equal "SAMPLE", System.get_env("SYSTEM_ENV_TEST_VAR")
  end

  test :cmd do
    assert is_binary(System.cmd "binary")
  end

  test :pwd do
    assert is_binary(System.pwd)
    assert_equal Regex.replace_all(%r/\n/, OS.cmd('pwd'), ""), binary_to_list(System.pwd)
  end
end


Wire up the schema in our router

Wire up the schema in our router

defmodule TimeTrackerBackend.Router do
  use TimeTrackerBackend.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug Guardian.Plug.VerifyHeader, realm: "Bearer"
    plug Guardian.Plug.LoadResource
  end

  forward "/graphql", Absinthe.Plug,
    schema: TimeTrackerBackend.Schema

  scope "/", TimeTrackerBackend do
    pipe_through :api

    resources "/users", UserController, except: [:new, :edit]
    resources "/projects", ProjectController, except: [:new, :edit]
    resources "/organizations", OrganizationController, except: [:new, :edit]
    resources "/sessions", SessionController, only: [:create]
    resources "/charts", ChartController, only: [:index]
  end
end

defmodule TimeTrackerBackend.Router do
  use TimeTrackerBackend.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug Guardian.Plug.VerifyHeader, realm: "Bearer"
    plug Guardian.Plug.LoadResource
  end

  scope "/", TimeTrackerBackend do
    pipe_through :api

    resources "/users", UserController, except: [:new, :edit]
    resources "/projects", ProjectController, except: [:new, :edit]
    resources "/organizations", OrganizationController, except: [:new, :edit]
    resources "/sessions", SessionController, only: [:create]
    resources "/charts", ChartController, only: [:index]
  end
end


Add a test for Xandra.TypeParser

Add a test for Xandra.TypeParser

defmodule Xandra.TypeParserTest do
  use ExUnit.Case, async: true

  import Xandra.TypeParser, only: [parse: 1]

  test "parse/1 with valid types" do
    assert parse("uuid") == :uuid
    assert parse("text") == :text
    assert parse("list<int>") == {:list, [:int]}
    assert parse("map<int, text>") == {:map, [:int, :text]}
    assert parse("set<map<int, list<text>>>") == {:set, [{:map, [:int, {:list, [:text]}]}]}

    assert parse("map<list<int>, map<int, int>>") ==
             {:map, [{:list, [:int]}, {:map, [:int, :int]}]}

    # Spaces here and there
    assert parse("map<int,text>") == {:map, [:int, :text]}
    assert parse("map<list<int>,map<int,int>>") == {:map, [{:list, [:int]}, {:map, [:int, :int]}]}

    assert parse(" map < int,   set < map < int, int  > > > ") ==
             {:map, [:int, {:set, [{:map, [:int, :int]}]}]}

    # Deviant case
    assert parse("TEXT") == :text
    assert parse("MAP<INT, SET<TEXT>>") == {:map, [:int, {:set, [:text]}]}
    assert parse("List<texT>") == {:list, [:text]}
  end

  test "parse/1 with invalid types" do
    assert_raise RuntimeError, ~s(invalid type: " "), fn ->
      parse(" ")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<>"), fn ->
      parse("list<>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int>>"), fn ->
      parse("list<int>>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int,>"), fn ->
      parse("list<int,>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list< ,int>"), fn ->
      parse("list< ,int>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int>, int"), fn ->
      parse("list<int>, int")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int"), fn ->
      parse("list<int")
    end
  end
end

defmodule Xandra.TypeParserTest do
  use ExUnit.Case, async: true

  import Xandra.TypeParser, only: [parse: 1]

  test "parse/1 with valid types" do
    assert parse("uuid") == :uuid
    assert parse("text") == :text
    assert parse("list<int>") == {:list, [:int]}
    assert parse("map<int, text>") == {:map, [:int, :text]}
    assert parse("set<map<int, list<text>>>") == {:set, [{:map, [:int, {:list, [:text]}]}]}

    assert parse("map<list<int>, map<int, int>>") ==
             {:map, [{:list, [:int]}, {:map, [:int, :int]}]}

    # Spaces here and there
    assert parse("map<int,text>") == {:map, [:int, :text]}
    assert parse("map<list<int>,map<int,int>>") == {:map, [{:list, [:int]}, {:map, [:int, :int]}]}

    assert parse(" map < int,   set < map < int, int  > > > ") ==
             {:map, [:int, {:set, [{:map, [:int, :int]}]}]}

    # Deviant case
    assert parse("TEXT") == :text
    assert parse("MAP<INT, SET<TEXT>>") == {:map, [:int, {:set, [:text]}]}
    assert parse("List<texT>") == {:list, [:text]}
  end

  test "parse/1 with invalid types" do
    assert_raise RuntimeError, ~s(invalid type: " "), fn ->
      parse(" ")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<>"), fn ->
      parse("list<>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int>>"), fn ->
      parse("list<int>>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int,>"), fn ->
      parse("list<int,>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list< ,int>"), fn ->
      parse("list< ,int>")
    end

    assert_raise RuntimeError, ~s(invalid type: "list<int>, int"), fn ->
      parse("list<int>, int")
    end
  end
end


Refactored parsing to look be more readable (IMO)

defmodule Dice do
  defmodule ParsedScalarTerm do
    defstruct operand: nil, value: 0

    def new([_, operand, value, rest]) do
      op = Dice.Parser.determine_operand(operand)
      %ParsedScalarTerm{operand: op, value: value}
    end
  end

  defmodule ParsedDiceTerm, do: defstruct operand: nil, quantity: 0, sides: 10
  defmodule ParsedComplexTerm, do: defstruct operand: nil, terms: []

  defmodule Parser do
    @valid_chars_regex ~r/^[0-9d\-\+\(]{1,1}[0-9d\-\+\*\/\(\)]*?[0-9\)]{1,1}$/i

    @multiple_dice_regex ~r/^([\+\-\*\/]?)(\d+?)d(\d+)(.*)/i
    @single_die_regex ~r/^([\+\-\*\/]?)d(\d+)(.*)/i
    @scalar_regex ~r/^([\+\-\*\/]?)(\d+)(.*)/i
    @complex_regex ~r/^([\+\-\*\/]?)[\(]{1,1}([0-9d\-\*\+\/\(\)]+)[\)]{1,1}([\+\-\*\/]?.*)/i

    def parse(dice_str) when is_binary(dice_str) and dice_str != "" do
        validation_result = _strip_spaces(dice_str)
                                        |> _validate

        case validation_result do
          {:ok, valid_str, []} ->
            _strip_spaces(valid_str)
              |> _parse

          {:error, _invalid_str, reason} ->
            IO.puts reason

          _ ->
            {:error, ["Unknown error!!!\n"]}
        end
    end

    defp _strip_spaces(dice_str) do
      String.replace(dice_str, " ", "")
    end

    defp _validate(dice_str) do
      {:ok, dice_str, []}
      |> _valid_chars
      |> _correct_num_of_parens
      |> _empty_parens_clauses
    end

    defp _valid_chars(validation_tuple = {_, dice_str, messages}) do
      valid_str = Regex.match?(@valid_chars_regex, dice_str)

      cond do
        !valid_str  -> {:error, dice_str, ["Dice string is not formatted correctly!\n" | messages]}
        valid_str   -> validation_tuple
      end
    end

    defp _correct_num_of_parens(validation_tuple = {_, dice_str, messages}) do
      parens_count_result = _count_parens(dice_str, 0)

      case parens_count_result do
          {:early_end, _} ->
            {:error, dice_str, ["Closing parenthesis does not match any opening parenthesis!\n" | messages]}

          {:full_scan, parens_count} when parens_count != 0 ->
            {:error, dice_str, ["Opening parenthesis does not match any closing parenthesis!\n" | messages]}

          _ -> validation_tuple
      end
    end

    defp _count_parens("", parens_count) do
      {:full_scan, parens_count}
    end

    defp _count_parens(_str, parens_count) when parens_count < 0 do
      {:early_end, parens_count}
    end

    defp _count_parens(<< "(", str :: binary >>, parens_count) do
      _count_parens(str, parens_count + 1)
    end

    defp _count_parens(<< ")", str :: binary >>, parens_count) do
      _count_parens(str, parens_count - 1)
    end

    defp _count_parens(<< _, str :: binary >>, parens_count) when is_binary(str) do
      _count_parens(str, parens_count)
    end

    defp _empty_parens_clauses(validation_tuple = {_, dice_str, messages}) do
      match = Regex.match?(~r/\(\)/i, dice_str)

      if match do
        {:error, dice_str, ["Empty clause in input!\n" | messages]}
      else
        validation_tuple
      end
    end

    defp _parse(dice_str, result \\ []) # Function head for defaults
    defp _parse(dice_str, result) do
      cond do
        match = Regex.run(@multiple_dice_regex, dice_str) ->
          [_, operand, quantity, sides, rest] = match
          {quant, _} = Integer.parse(quantity)
          {num_of_sides, _} = Integer.parse(sides)
          op = determine_operand(operand)
          _parse(rest, [%ParsedDiceTerm{operand: op, quantity: quant,  sides: num_of_sides} | result])

        match = Regex.run(@single_die_regex, dice_str) ->
          [_, operand, sides, rest] = match
          {num_of_sides, _} = Integer.parse(sides)
          op = determine_operand(operand)
          _parse(rest, [%ParsedDiceTerm{operand: op, quantity: 1,  sides: num_of_sides} | result])

        match = Regex.run(@scalar_regex, dice_str) ->
          [_, operand, value, rest] = match
          _parse(rest, [ParsedScalarTerm.new(match) | result])

        match = Regex.run(@complex_regex, dice_str) ->
          [_, operand, terms, rest] = match
          op = determine_operand(operand)
          _parse(rest, [%ParsedComplexTerm{operand: op, terms: _parse(terms)} | result])

        true ->
          Enum.reverse(result)
        end
    end

    def determine_operand("") do
      "+"
    end

    def determine_operand(operand) when is_binary(operand) and byte_size(operand) == 1 do
      operand
    end
  end
end

defmodule DieRoll do
  defmodule Scalar_Term, do: defstruct operand: nil, value: 0
  defmodule Die_Term, do: defstruct operand: nil, quantity: 0, sides: 10
  defmodule Complex_Term, do: defstruct operand: nil, terms: []

  def parse(dice_str) when is_binary(dice_str) and dice_str != "" do
      stripped_str = _strip_spaces(dice_str)
      validation_result = _validate(stripped_str)

      case validation_result do
        {:ok, []} ->
          IO.inspect _parse(stripped_str)
          IO.puts stripped_str
        {:error, reason} ->
          IO.puts reason
        _ ->
          {:error, ["Unknown error!!!\n"]}
      end
  end

  defp _validate(dice_str) do
    {:ok, []}
    |> _valid_chars?(dice_str)
    |> _correct_num_of_parens?(dice_str)
    |> _empty_parens_clauses?(dice_str)

  end

  defp _valid_chars?(validation_tuple = {_, messages}, dice_str) do
    valid_str = Regex.match?(~r/^[0-9d\-\+\(]{1,1}[0-9d\-\+\*\/\(\)]*?[0-9\)]{1,1}$/i, dice_str)

    cond do
      !valid_str  -> {:error, ["Dice string is not formatted correctly!\n" | messages]}
      valid_str   -> validation_tuple
    end
  end

  defp _correct_num_of_parens?(validation_tuple = {_, messages}, dice_str) do
    parens_count_result = _count_parens(dice_str, 0)

    cond do
        {:early_end, _parens_count} == parens_count_result ->
          {:error, ["Closing parenthesis does not match any opening parenthesis!\n" | messages]}

        {:full_scan, parens_count} == parens_count_result and parens_count != 0 ->
          {:error, ["Parentheses do not match!\n" | messages]}

        true -> validation_tuple
    end
  end

  defp _count_parens("", parens_count) do
    {:full_scan, parens_count}
  end

  defp _count_parens(_str, parens_count) when parens_count < 0 do
    {:early_end, parens_count}
  end

  defp _count_parens(<< "(", str :: binary >>, parens_count) do
    _count_parens(str, parens_count + 1)
  end

  defp _count_parens(<< ")", str :: binary >>, parens_count) do
    _count_parens(str, parens_count - 1)
  end

  defp _count_parens(<< _, str :: binary >>, parens_count) when is_binary(str) do
    _count_parens(str, parens_count)
  end

  defp _empty_parens_clauses?(validation_tuple = {_, messages}, dice_str) do
    match = Regex.match?(~r/\(\)/i, dice_str)

    if match do
      {:error, ["Empty clause in input!\n" | messages]}
    else
      validation_tuple
    end
  end

  defp _strip_spaces(dice_str) do
    String.replace(dice_str, " ", "")
  end

  defp _parse(dice_str, result \\ []) # Function head for defaults
  defp _parse(dice_str, result) do
    cond do
      # Die terms - Multiple dice
      match = Regex.run(~r/^([\+\-\*\/]?)(\d+?)d(\d+)(.*)/i, dice_str) ->
        [_, operand, quantity, sides, rest] = match
        IO.inspect match
        IO.inspect rest
        {quant, _} = Integer.parse(quantity)
        {num_of_sides, _} = Integer.parse(sides)
        # determine operand
        op = if operand == "", do: "+", else: operand

        _parse(rest, [%Die_Term{operand: op, quantity: quant,  sides: num_of_sides} | result])

      # Die terms - A single die
      match = Regex.run(~r/^([\+\-\*\/]?)d(\d+)(.*)/i, dice_str) ->
        [_, operand, sides, rest] = match
        IO.inspect match
        IO.inspect rest
        {num_of_sides, _} = Integer.parse(sides)
        # determine operand
        op = if operand == "", do: "+", else: operand

        _parse(rest, [%Die_Term{operand: op, quantity: 1,  sides: num_of_sides} | result])

      # Scalar terms
      match = Regex.run(~r/^([\+\-\*\/]?)(\d+)(.*)/i, dice_str) ->
        [_, operand, value, rest] = match
        IO.inspect match
        IO.inspect rest

        # determine operand
        op = if operand == "", do: "+", else: operand

        _parse(rest, [%Scalar_Term{operand: op, value: value} | result])

      # Complex terms
      match = Regex.run(~r/^([\+\-\*\/]?)[\(]{1,1}([0-9d\-\*\+\/\(\)]+)[\)]{1,1}([\+\-\*\/]?.*)/i, dice_str) ->
        [_, operand, terms, rest] = match
        IO.inspect match

        # determine operand
        op = if operand == "", do: "+", else: operand

        _parse(rest, [%Complex_Term{operand: op, terms: _parse(terms)} | result])

      true ->
        Enum.reverse(result)
    end
  end
end


Add persist_idea_for_retro/1 helper that provides default values

  - allows us to avoid adding @tag to each instance of persist_idea_for_retro

defmodule RemoteRetro.TestHelpers do
  use Wallaby.DSL
  alias RemoteRetro.{Repo, Idea}

  import ShorterMaps

  defp persist_idea(user, idea, retro, options \\ [assignee_id: nil]) do
    %Idea{
      assignee_id: options[:assignee_id],
      body: idea.body,
      category: idea.category,
      retro_id: retro.id,
      user_id: user.id
    } |> Repo.insert!
  end

  def persist_idea_for_retro(~M{idea, retro, facilitator} = context) do
    idea_author = Map.get(context, context[:idea_author] || :facilitator)

    idea =
      case idea.category == "action-item" do
        true -> persist_idea(idea_author, idea, retro, assignee_id: facilitator.id)
        false -> persist_idea(idea_author, idea, retro)
      end

    Map.put(context, :idea, idea)
  end

  def persist_idea_for_retro(~M{retro, facilitator} = context) do
    idea_author = Map.get(context, context[:idea_author] || :facilitator)

    idea =
      persist_idea(
        idea_author,
        %Idea{category: "confused", body: "whether I am alive right now? Is this real?"},
        retro
      )

    Map.put(context, :idea, idea)
  end

  def new_authenticated_browser_session(metadata \\ %{}) do
    :timer.sleep(100)
    {:ok, session} = Wallaby.start_session(metadata: metadata)
    authenticate(session)
  end

  def click_and_confirm(facilitator_session, button_text) do
    facilitator_session |> find(Query.button(button_text)) |> Element.click
    facilitator_session |> find(Query.button("Yes")) |> Element.click
  end

  def authenticate(session) do
    visit(session, "/auth/google/callback?code=love")
  end

  def submit_idea(session, ~M{category, body}) do
    session
    |> find(Query.css("form"))
    |> click(Query.option(category))
    |> fill_in(Query.text_field("idea"), with: body)
    |> click(Query.button("Submit"))

    session
  end

  def delete_idea(session, ~M{body}) do
    session
    |> stub_js_confirms
    |> find(Query.css(".ideas li", text: body))
    |> click(Query.css(".remove.icon"))
  end

  defp stub_js_confirms(session) do
    execute_script(session, "window.confirm = function(){ return true; }")
  end
end

defmodule RemoteRetro.TestHelpers do
  use Wallaby.DSL
  alias RemoteRetro.{Repo, Idea}

  import ShorterMaps

  defp persist_idea(user, idea, retro, options \\ [assignee_id: nil]) do
    %Idea{
      assignee_id: options[:assignee_id],
      body: idea.body,
      category: idea.category,
      retro_id: retro.id,
      user_id: user.id
    } |> Repo.insert!
  end

  def persist_idea_for_retro(~M{idea, retro, facilitator} = context) do
    idea_author = Map.get(context, context[:idea_author] || :facilitator)

    idea =
      case idea.category == "action-item" do
        true -> persist_idea(idea_author, idea, retro, assignee_id: facilitator.id)
        false -> persist_idea(idea_author, idea, retro)
      end

    Map.put(context, :idea, idea)
  end

  def new_authenticated_browser_session(metadata \\ %{}) do
    :timer.sleep(100)
    {:ok, session} = Wallaby.start_session(metadata: metadata)
    authenticate(session)
  end

  def click_and_confirm(facilitator_session, button_text) do
    facilitator_session |> find(Query.button(button_text)) |> Element.click
    facilitator_session |> find(Query.button("Yes")) |> Element.click
  end

  def authenticate(session) do
    visit(session, "/auth/google/callback?code=love")
  end

  def submit_idea(session, ~M{category, body}) do
    session
    |> find(Query.css("form"))
    |> click(Query.option(category))
    |> fill_in(Query.text_field("idea"), with: body)
    |> click(Query.button("Submit"))

    session
  end

  def delete_idea(session, ~M{body}) do
    session
    |> stub_js_confirms
    |> find(Query.css(".ideas li", text: body))
    |> click(Query.css(".remove.icon"))
  end

  defp stub_js_confirms(session) do
    execute_script(session, "window.confirm = function(){ return true; }")
  end
end


fix error

fix error

defmodule {{project_cap}}.Protocol.HTTP.Router.Helper do
  defmacro __using__(_) do
    quote do
      use Plug.Builder
      use Concerto, [root: "#{System.cwd!}/web",
                     ext: ".ex",
                     module_prefix: {{project_cap}}.Resource]

      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)

      def match(%{private: %{mazurka_route: _} } = conn, _opts) do
        conn
      end
      def match(%Plug.Conn{} = conn, _opts) do
        case match(conn.method, conn.path_info) do
          {module, params} ->
            conn
            |> put_private(:mazurka_route, module)
            |> put_private(:mazurka_resource, module)
            |> put_private(:mazurka_resource_params, %{})
            |> put_private(:mazurka_params, params)
          nil ->
            conn
            |> put_private(:mazurka_route, {{project_cap}}.Resource.Error.GET)
            |> put_private(:mazurka_resource, {{project_cap}}.Resource.Error.GET)
            |> put_private(:mazurka_resource_params, %{})
            |> put_private(:mazurka_params, %{})
        end
      end

      defp dispatch(%Plug.Conn{private: %{mazurka_route: route} } = conn, _opts) do
        Mazurka.Protocol.HTTP.Router.Handler.__handle__(conn, route)
      end
    end
  end

  for fun <- [:get, :post, :put, :delete] do
    method = fun |> to_string() |> String.upcase()
    defmacro unquote(fun)(path, module) do
      compile(unquote(method), path, module)
    end
  end

  defp compile(method, path, resource) do
    quote bind_quoted: [method: method,
                        path: path,
                        resource: resource] do

      {method, match, params, map_params, list_params} = Mazurka.Protocol.HTTP.Router.__route__(method, path)
      def match(unquote(method), unquote(match)) do
        {unquote(resource), unquote(map_params)}
      end

      {resolve_method, resolve_match} = Mazurka.Protocol.HTTP.Router.__resolve__(method, match)
      def resolve(unquote(resource), unquote(map_params)) do
        {unquote(resolve_method), unquote(resolve_match)}
      end
      def resolve(unquote(resource), unquote(list_params)) do
        {unquote(resolve_method), unquote(resolve_match)}
      end
    end
  end

  defmacro __before_compile__(_) do
    quote do
      defoverridable resolve: 2

      def resolve(resource, params) do
        case super(resource, params) do
          nil ->
            {:error, :not_found}
          {method, path_info} ->
            {:ok, method, path_info, %{} }
        end
      end
    end
  end
end

defmodule {{project_cap}}.Protocol.HTTP.Router.Helper do
  defmacro __using__(_) do
    quote do
      use Plug.Builder
      use Concerto, [root: "#{System.cwd!}/web",
                     ext: ".ex",
                     module_prefix: {{project_cap}}.Resource]

      import unquote(__MODULE__)
      @before_compile unquote(__MODULE__)

      def match(%{private: %{mazurka_route: _}} = conn, _opts) do
        conn
      end
      def match(%Plug.Conn{} = conn, _opts) do
        case match(conn.method, conn.path_info) do
          {module, params} ->
            conn
            |> put_private(:mazurka_route, module)
            |> put_private(:mazurka_resource, module)
            |> put_private(:mazurka_resource_params, %{})
            |> put_private(:mazurka_params, params)
          nil ->
            conn
            |> put_private(:mazurka_route, {{project_cap}}.Resource.Error.GET)
            |> put_private(:mazurka_resource, {{project_cap}}.Resource.Error.GET)
            |> put_private(:mazurka_resource_params, %{})
            |> put_private(:mazurka_params, %{})
        end
      end

      defp dispatch(%Plug.Conn{private: %{mazurka_route: route}} = conn, _opts) do
        Mazurka.Protocol.HTTP.Router.Handler.__handle__(conn, route)
      end
    end
  end

  for fun <- [:get, :post, :put, :delete] do
    method = fun |> to_string() |> String.upcase()
    defmacro unquote(fun)(path, module) do
      compile(unquote(method), path, module)
    end
  end

  defp compile(method, path, resource) do
    quote bind_quoted: [method: method,
                        path: path,
                        resource: resource] do

      {method, match, params, map_params, list_params} = Mazurka.Protocol.HTTP.Router.__route__(method, path)
      def match(unquote(method), unquote(match)) do
        {unquote(resource), unquote(map_params)}
      end

      {resolve_method, resolve_match} = Mazurka.Protocol.HTTP.Router.__resolve__(method, match)
      def resolve(unquote(resource), unquote(map_params)) do
        {unquote(resolve_method), unquote(resolve_match)}
      end
      def resolve(unquote(resource), unquote(list_params)) do
        {unquote(resolve_method), unquote(resolve_match)}
      end
    end
  end

  defmacro __before_compile__(_) do
    quote do
      defoverridable resolve: 2

      def resolve(resource, params) do
        case super(resource, params) do
          nil ->
            {:error, :not_found}
          {method, path_info} ->
            {:ok, method, path_info, %{}}
        end
      end
    end
  end
end


Remove unused alias

Remove unused alias

defmodule Enricher.LabelsTest do
  use ExUnit.Case, async: true
  use Plug.Test
  alias Enricher.Labels
  alias Core.ProjectConfigLoader

  test "add labels" do

    change = %{
      :doc => %{
        :resource => %{
          :identifier => "ABC",
          :shortDescription => "Test resource",
          :category => "Operation",
          "color" => ["Grün", "Blau"],
          "material" => ["Eisen"],
          "width" => [%{ "inputValue" => 10, "inputUnit" => "cm", "measurementPosition" => "Maximale Ausdehnung" }],
          "period" => %{ "value" => "Old Babylonian", "endValue" => "New Babylonian" },
          :id => "42",
          :relations => %{ "liesWithin" => ["45"] }
        }
      }
    }

    start_supervised({Core.ProjectConfigLoader, {"test/resources", ["test-project"]}})
    configuration = ProjectConfigLoader.get("test-project")

    result = Labels.add_labels(change, configuration)

    assert result == %{
      :doc => %{
        :resource => %{
          id: "42",
          identifier: "ABC",
          shortDescription: "Test resource",
          category: %{ name: "Operation", label: %{ de: "Maßnahme", en: "Operation" } },
          color: [
            %{ name: "Grün", label: %{} }, # Value definition found, but does not include label
            %{ name: "Blau", label: %{ de: "Blau", en: "Blue" } } # Value definition with label found
          ],
          material: [
            %{ name: "Eisen", label: %{} } # No value definition found
          ],
          width: [
            %{
              inputValue: 10,
              inputUnit: "cm",
              measurementPosition: %{
               name: "Maximale Ausdehnung",
               label: %{
                 de: "Maximale Ausdehnung",
                 en: "Maximum expansion"
                }
              }
            }
          ],
          period: %{
            value: %{
              name: "Old Babylonian",
              label: %{
                de: "Altbabylonisch",
                en: "Old Babylonian"
              }
            },
            endValue: %{
              name: "New Babylonian",
              label: %{
                de: "Neubabylonisch",
                en: "New Babylonian"
              }
            }
          },
          relations: %{ liesWithin: ["45"] }
        }
      }
    }
  end
end

defmodule Enricher.LabelsTest do
  use ExUnit.Case, async: true
  use Plug.Test
  alias Enricher.Labels
  alias Core.ProjectConfigLoader
  alias Core.CategoryTreeList

  test "add labels" do

    change = %{
      :doc => %{
        :resource => %{
          :identifier => "ABC",
          :shortDescription => "Test resource",
          :category => "Operation",
          "color" => ["Grün", "Blau"],
          "material" => ["Eisen"],
          "width" => [%{ "inputValue" => 10, "inputUnit" => "cm", "measurementPosition" => "Maximale Ausdehnung" }],
          "period" => %{ "value" => "Old Babylonian", "endValue" => "New Babylonian" },
          :id => "42",
          :relations => %{ "liesWithin" => ["45"] }
        }
      }
    }

    start_supervised({Core.ProjectConfigLoader, {"test/resources", ["test-project"]}})
    configuration = ProjectConfigLoader.get("test-project")

    result = Labels.add_labels(change, configuration)

    assert result == %{
      :doc => %{
        :resource => %{
          id: "42",
          identifier: "ABC",
          shortDescription: "Test resource",
          category: %{ name: "Operation", label: %{ de: "Maßnahme", en: "Operation" } },
          color: [
            %{ name: "Grün", label: %{} }, # Value definition found, but does not include label
            %{ name: "Blau", label: %{ de: "Blau", en: "Blue" } } # Value definition with label found
          ],
          material: [
            %{ name: "Eisen", label: %{} } # No value definition found
          ],
          width: [
            %{
              inputValue: 10,
              inputUnit: "cm",
              measurementPosition: %{
               name: "Maximale Ausdehnung",
               label: %{
                 de: "Maximale Ausdehnung",
                 en: "Maximum expansion"
                }
              }
            }
          ],
          period: %{
            value: %{
              name: "Old Babylonian",
              label: %{
                de: "Altbabylonisch",
                en: "Old Babylonian"
              }
            },
            endValue: %{
              name: "New Babylonian",
              label: %{
                de: "Neubabylonisch",
                en: "New Babylonian"
              }
            }
          },
          relations: %{ liesWithin: ["45"] }
        }
      }
    }
  end
end


Move source_ref ex_doc config to docs section

defmodule Tesla.Mixfile do
  use Mix.Project

  @version "1.3.0"

  def project do
    [
      app: :tesla,
      version: @version,
      description: description(),
      package: package(),
      source_url: "https://github.com/teamon/tesla",
      elixir: "~> 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      deps: deps(),
      lockfile: lockfile(System.get_env("LOCKFILE")),
      test_coverage: [tool: ExCoveralls],
      dialyzer: [
        plt_add_apps: [:inets],
        plt_add_deps: :project
      ],
      docs: docs()
    ]
  end

  # Configuration for the OTP application
  #
  # Type `mix help compile.app` for more information
  def application do
    [applications: applications(Mix.env())]
  end

  def applications(:test), do: applications(:dev) ++ [:httparrot, :hackney, :ibrowse, :gun]
  def applications(_), do: [:logger, :ssl, :inets]

  defp description do
    "HTTP client library, with support for middleware and multiple adapters."
  end

  defp package do
    [
      maintainers: ["Tymon Tobolski"],
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/teamon/tesla"}
    ]
  end

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

  defp lockfile(nil), do: "mix.lock"
  defp lockfile(lockfile), do: "test/lockfiles/#{lockfile}.lock"

  defp deps do
    [
      {:mime, "~> 1.0"},

      # http clients
      {:ibrowse, "~> 4.4.0", optional: true},
      {:hackney, "~> 1.6", optional: true},
      {:gun, "~> 1.3", optional: true},
      {:castore, "~> 0.1", optional: true},
      {:mint, "~> 1.0", optional: true},

      # json parsers
      {:jason, ">= 1.0.0", optional: true},
      {:poison, ">= 1.0.0", optional: true},
      {:exjsx, ">= 3.0.0", optional: true},

      # other
      {:fuse, "~> 2.4", optional: true},
      {:telemetry, "~> 0.3", optional: true},

      # testing & docs
      {:excoveralls, "~> 0.8", only: :test},
      {:httparrot, "~> 1.2", only: :test},
      {:ex_doc, "~> 0.21", only: :dev},
      {:mix_test_watch, "~> 1.0", only: :dev},
      {:dialyxir, "~> 1.0.0-rc.3", only: [:dev, :test]},
      {:inch_ex, "~> 0.5.6", only: :docs}
    ]
  end

  defp docs do
    [
      main: "readme",
      source_ref: "v#{@version}",
      extras: ["README.md"],
      groups_for_modules: [
        Behaviours: [
          Tesla.Adapter,
          Tesla.Middleware
        ],
        Adapters: [
          Tesla.Adapter.Gun,
          Tesla.Adapter.Hackney,
          Tesla.Adapter.Httpc,
          Tesla.Adapter.Ibrowse,
          Tesla.Adapter.Mint
        ],
        Middlewares: [
          Tesla.Middleware.BaseUrl,
          Tesla.Middleware.BasicAuth,
          Tesla.Middleware.Compression,
          Tesla.Middleware.CompressRequest,
          Tesla.Middleware.DecodeJson,
          Tesla.Middleware.DecodeRels,
          Tesla.Middleware.DecompressResponse,
          Tesla.Middleware.DigestAuth,
          Tesla.Middleware.EncodeJson,
          Tesla.Middleware.FollowRedirects,
          Tesla.Middleware.FormUrlencoded,
          Tesla.Middleware.Fuse,
          Tesla.Middleware.Headers,
          Tesla.Middleware.JSON,
          Tesla.Middleware.KeepRequest,
          Tesla.Middleware.Logger,
          Tesla.Middleware.MethodOverride,
          Tesla.Middleware.Opts,
          Tesla.Middleware.PathParams,
          Tesla.Middleware.Query,
          Tesla.Middleware.Retry,
          Tesla.Middleware.Telemetry,
          Tesla.Middleware.Timeout
        ]
      ],
      nest_modules_by_prefix: [
        Tesla.Adapter,
        Tesla.Middleware
      ]
    ]
  end
end

defmodule Tesla.Mixfile do
  use Mix.Project

  @version "1.3.0"

  def project do
    [
      app: :tesla,
      version: @version,
      description: description(),
      package: package(),
      source_ref: "v#{@version}",
      source_url: "https://github.com/teamon/tesla",
      elixir: "~> 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      deps: deps(),
      lockfile: lockfile(System.get_env("LOCKFILE")),
      test_coverage: [tool: ExCoveralls],
      dialyzer: [
        plt_add_apps: [:inets],
        plt_add_deps: :project
      ],
      docs: docs()
    ]
  end

  # Configuration for the OTP application
  #
  # Type `mix help compile.app` for more information
  def application do
    [applications: applications(Mix.env())]
  end

  def applications(:test), do: applications(:dev) ++ [:httparrot, :hackney, :ibrowse, :gun]
  def applications(_), do: [:logger, :ssl, :inets]

  defp description do
    "HTTP client library, with support for middleware and multiple adapters."
  end

  defp package do
    [
      maintainers: ["Tymon Tobolski"],
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/teamon/tesla"}
    ]
  end

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

  defp lockfile(nil), do: "mix.lock"
  defp lockfile(lockfile), do: "test/lockfiles/#{lockfile}.lock"

  defp deps do
    [
      {:mime, "~> 1.0"},

      # http clients
      {:ibrowse, "~> 4.4.0", optional: true},
      {:hackney, "~> 1.6", optional: true},
      {:gun, "~> 1.3", optional: true},
      {:castore, "~> 0.1", optional: true},
      {:mint, "~> 1.0", optional: true},

      # json parsers
      {:jason, ">= 1.0.0", optional: true},
      {:poison, ">= 1.0.0", optional: true},
      {:exjsx, ">= 3.0.0", optional: true},

      # other
      {:fuse, "~> 2.4", optional: true},
      {:telemetry, "~> 0.3", optional: true},

      # testing & docs
      {:excoveralls, "~> 0.8", only: :test},
      {:httparrot, "~> 1.2", only: :test},
      {:ex_doc, "~> 0.21", only: :dev},
      {:mix_test_watch, "~> 1.0", only: :dev},
      {:dialyxir, "~> 1.0.0-rc.3", only: [:dev, :test]},
      {:inch_ex, "~> 0.5.6", only: :docs}
    ]
  end

  defp docs do
    [
      main: "readme",
      extras: ["README.md"],
      groups_for_modules: [
        Behaviours: [
          Tesla.Adapter,
          Tesla.Middleware
        ],
        Adapters: [
          Tesla.Adapter.Gun,
          Tesla.Adapter.Hackney,
          Tesla.Adapter.Httpc,
          Tesla.Adapter.Ibrowse,
          Tesla.Adapter.Mint
        ],
        Middlewares: [
          Tesla.Middleware.BaseUrl,
          Tesla.Middleware.BasicAuth,
          Tesla.Middleware.Compression,
          Tesla.Middleware.CompressRequest,
          Tesla.Middleware.DecodeJson,
          Tesla.Middleware.DecodeRels,
          Tesla.Middleware.DecompressResponse,
          Tesla.Middleware.DigestAuth,
          Tesla.Middleware.EncodeJson,
          Tesla.Middleware.FollowRedirects,
          Tesla.Middleware.FormUrlencoded,
          Tesla.Middleware.Fuse,
          Tesla.Middleware.Headers,
          Tesla.Middleware.JSON,
          Tesla.Middleware.KeepRequest,
          Tesla.Middleware.Logger,
          Tesla.Middleware.MethodOverride,
          Tesla.Middleware.Opts,
          Tesla.Middleware.PathParams,
          Tesla.Middleware.Query,
          Tesla.Middleware.Retry,
          Tesla.Middleware.Telemetry,
          Tesla.Middleware.Timeout
        ]
      ],
      nest_modules_by_prefix: [
        Tesla.Adapter,
        Tesla.Middleware
      ]
    ]
  end
end