Fix example

Fix example

defmodule Floki do
  @moduledoc """
  A HTML parser and seeker.

  This is a simple HTML parser that enables searching using CSS like selectors.

  You can search elements by class, tag name and id.

  ## Example

  Assuming that you have the following HTML:

      <!doctype html>

Floki

          Github page

  You can perform the following queries:

    * Floki.find(html, "#content") : returns the section with all children;
    * Floki.find(html, ".headline") : returns a list with the `p` element;
    * Floki.find(html, "a") : returns a list with the `a` element;
    * Floki.find(html, "#content a") # returns all links inside content section;
    * Floki.find(html, ".headline, a") # returns the .headline elements and links.

  Each HTML node is represented by a tuple like:

      {tag_name, attributes, children_nodes}

  Example of node:

      {"p", [{"class", "headline"}], ["Floki"]}

  So even if the only child node is the element text, it is represented
  inside a list.

  You can write a simple HTML crawler (with support of [HTTPoison](https://github.com/edgurgel/httpoison)) with a few lines of code:

      html
      |> Floki.find(".pages a")
      |> Floki.attribute("href")
      |> Enum.map(fn(url) -> HTTPoison.get!(url) end)

  It is simple as that!
  """

  @type html_tree :: tuple | list

  @doc """
  Parses a HTML string.

  ## Examples

      iex> Floki.parse("
hello world
")
      {"div", [{"class", "js-action"}], ["hello world"]}

  """

  @spec parse(binary) :: html_tree

  def parse(html), do: :mochiweb_html.parse(html)

  @doc """
  Finds elements inside a HTML tree or string.
  You can search by class, tag name or id.

  It is possible to compose searches:

      Floki.find(html_string, ".class")
      |> Floki.find(".another-class-inside-small-scope")

  ## Examples

      iex> Floki.find("
hello
", ".hint")
      [{"span", [{"class", "hint"}], ["hello"]}]

      iex> "

Content

" |> Floki.find("#important")
      {"div", [{"id", "important"}], [{"div", [], ["Content"]}]}

      iex> Floki.find("
Google
", "a")
      [{"a", [{"href", "https://google.com"}], ["Google"]}]

  """

  @spec find(binary | html_tree, binary) :: html_tree

  def find(html, selector) when is_binary(html) do
    html_tree = parse(html)

    find(html_tree, selector)
  end

  def find(html_tree, selector) do
    cond do
      String.contains?(selector, ",") ->
        selectors = String.split(selector, ",")

        Enum.reduce selectors, [], fn(selector, acc) ->
          selector = String.strip(selector)

          nodes = find(html_tree, selector)

          unless is_list(nodes), do: nodes = [nodes]

          Enum.concat(acc, nodes)
        end
      String.contains?(selector, "\s") ->
        descendent_selector = String.split(selector)

        Enum.reduce descendent_selector, html_tree, fn(selector, tree) ->
          find(tree, selector)
        end
      String.starts_with?(selector, ".") ->
        "." <> class = selector
        {:ok, nodes} = find_by_selector(class, html_tree, &class_matcher/3, {:ok, []})

        Enum.reverse(nodes)
      String.starts_with?(selector, "#") ->
        "#" <> id = selector
        {_status, nodes} = find_by_selector(id, html_tree, &id_matcher/3, {:ok, []})

        List.first(nodes)
      true ->
        {:ok, nodes} = find_by_selector(selector, html_tree, &tag_matcher/3, {:ok, []})

        Enum.reverse(nodes)
    end
  end

  @doc """
  Returns a list with attribute values for a given selector.

  ## Examples

      iex> Floki.attribute("Google", "a", "href")
      ["https://google.com"]

  """

  @spec attribute(binary | html_tree, binary, binary) :: list

  def attribute(html, selector, attribute_name) do
    html
    |> find(selector)
    |> attribute_values(attribute_name)
  end

  @doc """
  Returns a list with attribute values from elements.

  ## Examples

      iex> Floki.attribute("Google", "href")
      ["https://google.com"]

  """

  @spec attribute(binary | html_tree, binary) :: list

  def attribute(html_tree, attribute_name) when is_binary(html_tree) do
    html_tree
    |> parse
    |> attribute(attribute_name)
  end
  def attribute(elements, attribute_name) do
    elements
    |> attribute_values(attribute_name)
  end

  @doc """
  Returns the text nodes from a HTML tree.
  By default, it will perform a deep search through the HTML tree.
  You can disable deep search with the option `deep` assigned to false.

  ## Examples

      iex> Floki.text("
hello world
")
      "hello world"

      iex> Floki.text("
hello world
", deep: false)
      " world"

  """

  @spec text(html_tree | binary) :: binary

  def text(html, opts \\ [deep: true]) do
    html_tree =
      case is_binary(html) do
        true -> parse(html)
        false -> html
      end

    search_strategy =
      case opts[:deep] do
        true -> Floki.DeepText
        false -> Floki.FlatText
      end

    search_strategy.get(html_tree)
  end

  defp attribute_match?(attributes, attribute_name) do
    Enum.find attributes, fn({attr_name, _}) ->
      attr_name == attribute_name
    end
  end

  defp attribute_match?(attributes, attribute_name, selector_value) do
    Enum.find attributes, fn(attribute) ->
      {attr_name, attr_value} = attribute

      attr_name == attribute_name && value_match?(attr_value, selector_value)
    end
  end

  defp find_by_selector(_selector, {}, _, acc), do: acc
  defp find_by_selector(_selector, [], _, acc), do: acc
  defp find_by_selector(_selector, _, _, {:done, nodes}), do: {:done, nodes}
  defp find_by_selector(_selector, tree, _, acc) when is_binary(tree), do: acc
  defp find_by_selector(selector, [h|t], matcher, acc) do
    acc = find_by_selector(selector, h, matcher, acc)
    find_by_selector(selector, t, matcher, acc)
  end
  # Ignores comments
  defp find_by_selector(_selector, {:comment, _comment}, _, acc), do: acc
  defp find_by_selector(selector, node, matcher, acc) do
    {_, _, child_node} = node

    acc = matcher.(selector, node, acc)

    find_by_selector(selector, child_node, matcher, acc)
  end

  defp attribute_values(element, attr_name) when is_tuple(element) do
    attribute_values([element], attr_name)
  end
  defp attribute_values(elements, attr_name) do
    values = Enum.reduce elements, [], fn({_, attributes, _}, acc) ->
      case attribute_match?(attributes, attr_name) do
        {_attr_name, value} ->
          [value|acc]
        _ ->
          acc
      end
    end

    Enum.reverse values
  end

  defp class_matcher(class_name, node, acc) do
    {_, attributes, _} = node
    {:ok, acc_nodes} = acc

    if attribute_match?(attributes, "class", class_name) do
      acc = {:ok, [node|acc_nodes]}
    end

    acc
  end

  defp tag_matcher(tag_name, node, acc) do
    {tag, _, _} = node
    {:ok, acc_nodes} = acc

    if tag == tag_name do
      acc = {:ok, [node|acc_nodes]}
    end

    acc
  end

  defp id_matcher(id, node, acc) do
    {_, attributes, _} = node
    {:ok, acc_nodes} = acc

    if attribute_match?(attributes, "id", id) do
      acc = {:done, [node|acc_nodes]}
    end

    acc
  end

  defp value_match?(attribute_value, selector_value) do
    attribute_value
    |> String.split
    |> Enum.any?(fn(x) -> x == selector_value end)
  end
end

defmodule Floki do
  @moduledoc """
  A HTML parser and seeker.

  This is a simple HTML parser that enables searching using CSS like selectors.

  You can search elements by class, tag name and id.

  ## Example

  Assuming that you have the following HTML:

      <!doctype html>

Floki

          Github page

  You can perform the following queries:

    * Floki.find(html, "#content") : returns the section with all children;
    * Floki.find(html, ".headline") : returns a list with the `p` element;
    * Floki.find(html, "a") : returns a list with the `a` element;
    * Floki.find(html, "#content a") # returns all links inside content section;
    * Floki.find(html, ".headline, a") # returns the .headline elements and links.

  Each HTML node is represented by a tuple like:

      {tag_name, attributes, children_nodes}

  Example of node:

      {"p", [{"class", "headline"}], ["Floki"]}

  So even if the only child node is the element text, it is represented
  inside a list.

  You can write a simple HTML crawler (with support of [HTTPoison](https://github.com/edgurgel/httpoison)) with a few lines of code:

      html
      |> Floki.find(".pages")
      |> Floki.find("a")
      |> Floki.attribute("href")
      |> Enum.map(fn(url) -> HTTPoison.get!(url) end)

  It is simple as that!
  """

  @type html_tree :: tuple | list

  @doc """
  Parses a HTML string.

  ## Examples

      iex> Floki.parse("
hello world
")
      {"div", [{"class", "js-action"}], ["hello world"]}

  """

  @spec parse(binary) :: html_tree

  def parse(html), do: :mochiweb_html.parse(html)

  @doc """
  Finds elements inside a HTML tree or string.
  You can search by class, tag name or id.

  It is possible to compose searches:

      Floki.find(html_string, ".class")
      |> Floki.find(".another-class-inside-small-scope")

  ## Examples

      iex> Floki.find("
hello
", ".hint")
      [{"span", [{"class", "hint"}], ["hello"]}]

      iex> "

Content

" |> Floki.find("#important")
      {"div", [{"id", "important"}], [{"div", [], ["Content"]}]}

      iex> Floki.find("
Google
", "a")
      [{"a", [{"href", "https://google.com"}], ["Google"]}]

  """

  @spec find(binary | html_tree, binary) :: html_tree

  def find(html, selector) when is_binary(html) do
    html_tree = parse(html)

    find(html_tree, selector)
  end

  def find(html_tree, selector) do
    cond do
      String.contains?(selector, ",") ->
        selectors = String.split(selector, ",")

        Enum.reduce selectors, [], fn(selector, acc) ->
          selector = String.strip(selector)

          nodes = find(html_tree, selector)

          unless is_list(nodes), do: nodes = [nodes]

          Enum.concat(acc, nodes)
        end
      String.contains?(selector, "\s") ->
        descendent_selector = String.split(selector)

        Enum.reduce descendent_selector, html_tree, fn(selector, tree) ->
          find(tree, selector)
        end
      String.starts_with?(selector, ".") ->
        "." <> class = selector
        {:ok, nodes} = find_by_selector(class, html_tree, &class_matcher/3, {:ok, []})

        Enum.reverse(nodes)
      String.starts_with?(selector, "#") ->
        "#" <> id = selector
        {_status, nodes} = find_by_selector(id, html_tree, &id_matcher/3, {:ok, []})

        List.first(nodes)
      true ->
        {:ok, nodes} = find_by_selector(selector, html_tree, &tag_matcher/3, {:ok, []})

        Enum.reverse(nodes)
    end
  end

  @doc """
  Returns a list with attribute values for a given selector.

  ## Examples

      iex> Floki.attribute("Google", "a", "href")
      ["https://google.com"]

  """

  @spec attribute(binary | html_tree, binary, binary) :: list

  def attribute(html, selector, attribute_name) do
    html
    |> find(selector)
    |> attribute_values(attribute_name)
  end

  @doc """
  Returns a list with attribute values from elements.

  ## Examples

      iex> Floki.attribute("Google", "href")
      ["https://google.com"]

  """

  @spec attribute(binary | html_tree, binary) :: list

  def attribute(html_tree, attribute_name) when is_binary(html_tree) do
    html_tree
    |> parse
    |> attribute(attribute_name)
  end
  def attribute(elements, attribute_name) do
    elements
    |> attribute_values(attribute_name)
  end

  @doc """
  Returns the text nodes from a HTML tree.
  By default, it will perform a deep search through the HTML tree.
  You can disable deep search with the option `deep` assigned to false.

  ## Examples

      iex> Floki.text("
hello world
")
      "hello world"

      iex> Floki.text("
hello world
", deep: false)
      " world"

  """

  @spec text(html_tree | binary) :: binary

  def text(html, opts \\ [deep: true]) do
    html_tree =
      case is_binary(html) do
        true -> parse(html)
        false -> html
      end

    search_strategy =
      case opts[:deep] do
        true -> Floki.DeepText
        false -> Floki.FlatText
      end

    search_strategy.get(html_tree)
  end

  defp attribute_match?(attributes, attribute_name) do
    Enum.find attributes, fn({attr_name, _}) ->
      attr_name == attribute_name
    end
  end

  defp attribute_match?(attributes, attribute_name, selector_value) do
    Enum.find attributes, fn(attribute) ->
      {attr_name, attr_value} = attribute

      attr_name == attribute_name && value_match?(attr_value, selector_value)
    end
  end

  defp find_by_selector(_selector, {}, _, acc), do: acc
  defp find_by_selector(_selector, [], _, acc), do: acc
  defp find_by_selector(_selector, _, _, {:done, nodes}), do: {:done, nodes}
  defp find_by_selector(_selector, tree, _, acc) when is_binary(tree), do: acc
  defp find_by_selector(selector, [h|t], matcher, acc) do
    acc = find_by_selector(selector, h, matcher, acc)
    find_by_selector(selector, t, matcher, acc)
  end
  # Ignores comments
  defp find_by_selector(_selector, {:comment, _comment}, _, acc), do: acc
  defp find_by_selector(selector, node, matcher, acc) do
    {_, _, child_node} = node

    acc = matcher.(selector, node, acc)

    find_by_selector(selector, child_node, matcher, acc)
  end

  defp attribute_values(element, attr_name) when is_tuple(element) do
    attribute_values([element], attr_name)
  end
  defp attribute_values(elements, attr_name) do
    values = Enum.reduce elements, [], fn({_, attributes, _}, acc) ->
      case attribute_match?(attributes, attr_name) do
        {_attr_name, value} ->
          [value|acc]
        _ ->
          acc
      end
    end

    Enum.reverse values
  end

  defp class_matcher(class_name, node, acc) do
    {_, attributes, _} = node
    {:ok, acc_nodes} = acc

    if attribute_match?(attributes, "class", class_name) do
      acc = {:ok, [node|acc_nodes]}
    end

    acc
  end

  defp tag_matcher(tag_name, node, acc) do
    {tag, _, _} = node
    {:ok, acc_nodes} = acc

    if tag == tag_name do
      acc = {:ok, [node|acc_nodes]}
    end

    acc
  end

  defp id_matcher(id, node, acc) do
    {_, attributes, _} = node
    {:ok, acc_nodes} = acc

    if attribute_match?(attributes, "id", id) do
      acc = {:done, [node|acc_nodes]}
    end

    acc
  end

  defp value_match?(attribute_value, selector_value) do
    attribute_value
    |> String.split
    |> Enum.any?(fn(x) -> x == selector_value end)
  end
end


format cache

format cache

defmodule Neoprice.Cache do
  @moduledoc "macro to define a buffer"

  use GenServer
  alias Neoprice.Cryptocompare
  require Logger

  @minute 60
  @hour 3600
  @day 86_400

  defmodule Config do
    @moduledoc false
    defstruct [:cache_name, :definition, :aggregation, :duration]
  end

  defmacro __using__(opts \\ []) do
    quote do

      def worker do
        import Supervisor.Spec

        state = %{
          module: __MODULE__
        }
        worker(unquote(__MODULE__), [state], id: __MODULE__)
      end

      def from_symbol, do: unquote(
        if is_nil(opts[:from_symbol]), do: "BTC", else: opts[:from_symbol]
      )
      def to_symbol, do: unquote(
        if is_nil(opts[:to_symbol]), do: "BTC", else: opts[:to_symbol]
      )
      def config, do: unquote(
        if is_nil(opts[:config]), do: [], else: opts[:config]
      )
      def start_day, do: unquote(
        if is_nil(opts[:start_day]), do: 1_500_000_000, else: opts[:start_day]
      )
      def price, do: unquote(__MODULE__).price(__MODULE__)
      def last_price_full, do: unquote(__MODULE__).last_price_full(__MODULE__)
    end
  end

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

  def init(state) do
    Enum.each(
      state.module.config,
      fn (cache) ->
        :ets.new(
          cache.cache_name,
          [:public, :ordered_set, :named_table, {:read_concurrency, true}]
        )
      end
    )
    Process.send_after(self(), :seed, 0)
    {:ok, state}
  end

  def handle_info(:seed, state) do
    Process.send_after(self(), :sync, 10_000)
    seed(state)
    {:noreply, state}
  end

  def handle_info(:sync, state) do
    Process.send_after(self(), :sync, 10_000)
    sync(state)
    {:noreply, state}
  end

  def price(module) do
    Cryptocompare.last_price(module.from_symbol(), module.to_symbol())
  end

  def last_price_full(module) do
    Cryptocompare.last_price_full(module.from_symbol(), module.to_symbol())
  end

  defp seed(state) do
    Enum.each(
      state.module.config,
      fn (cache) ->
        seed(state.module, cache)
      end
    )
  end

  defp seed(module, cache) do
    {from, to} = time_frame(module, cache)
    elements = Cryptocompare.get_price(
      cache.definition,
      from,
      to,
      module.from_symbol(),
      module.to_symbol(),
      cache.aggregation
    )
    :ets.insert(cache.cache_name, elements)
  end

  def sync(state) do
    Enum.each(
      state.module.config,
      fn (cache) ->
        sync_cache(cache, state.module)
      end
    )
  end

  defp sync_cache(
         %{
           cache_name: cache_name,
           definition: definition,
           aggregation: aggregation
         } = config,
         module
       ) do
    cache = :ets.tab2list(cache_name)
    {last_time, _} = List.last(cache) || {0, ""}
    if next_value(definition, last_time, aggregation) < now() do
      Logger.debug fn ->
        "Syncing #{cache_name}"
      end
      elements = Cryptocompare.get_price(
        definition,
        last_time + 1,
        now(),
        module.from_symbol,
        module.to_symbol,
        aggregation
      )
      :ets.insert(cache_name, elements)
      delete_old_values(config, module, cache)
    end
  end

  defp time_frame(module, %{duration: :start}), do: {module.start_day, now()}
  defp time_frame(_, %{duration: duration}) do
    now = now()
    {now - duration, now}
  end

  defp delete_old_values(config, module, cache) do
    {from, _} = time_frame(module, config)
    Enum.reduce_while(
      cache,
      nil,
      fn ({k, _}, _) ->
        if k < from do
          :ets.delete(config.cache_name, k)
          Logger.debug fn ->
            "Deleteting #{k}"
          end
          {:cont, nil}
        else
          {:halt, nil}
        end
      end
    )
  end

  defp next_value(:day, time, aggregation), do: time + @day * aggregation
  defp next_value(:hour, time, aggregation), do: time + @hour * aggregation
  defp next_value(:minute, time, aggregation), do: time + @minute * aggregation

  defp now, do: DateTime.utc_now()  |> DateTime.to_unix()
end


way to disable generating docs

way to disable generating docs

defmodule Tesla.Error do
  defexception message: "", reason: nil
end

defmodule Tesla.Env do
  @type client      :: (t,stack -> t)
  @type method      :: :head | :get | :delete | :trace | :options | :post | :put | :patch
  @type url         :: binary
  @type param       :: binary | [{(binary | atom), param}]
  @type query       :: [{(binary | atom), param}]
  @type headers     :: %{binary => binary}
  @type body        :: any #
  @type status      :: integer
  @type opts        :: [any]
  @type __module__  :: atom
  @type __client__  :: function

  @type stack       :: [{atom, atom, any}]

  @type t :: %__MODULE__{
            method:     method,
            query:      query,
            url:        url,
            headers:    headers,
            body:       body,
            status:     status,
            opts:       opts,
            __module__: __module__,
            __client__: __client__
  }

  defstruct method:     nil,
            url:        "",
            query:      [],
            headers:    %{},
            body:       nil,
            status:     nil,
            opts:       [],
            __module__: nil,
            __client__: nil
end

defmodule Tesla.Builder do
  @http_verbs ~w(head get delete trace options post put patch)a

  defmacro __using__(opts \\ []) do
    opts = Macro.prewalk(opts, &Macro.expand(&1, __CALLER__))

    quote do
      Module.register_attribute(__MODULE__, :__middleware__, accumulate: true)
      Module.register_attribute(__MODULE__, :__adapter__, [])

      docs = Keyword.get(unquote(opts), :docs, true)

      if docs do
        @type option :: {:method,   Tesla.Env.method}  |
                        {:url,      Tesla.Env.url}     |
                        {:query,    Tesla.Env.query}   |
                        {:headers,  Tesla.Env.headers} |
                        {:body,     Tesla.Env.body}    |
                        {:opts,     Tesla.Env.opts}

        @doc """
        Perform a request using client function

        Options:
        - `:method`   - the request method, one of [:head, :get, :delete, :trace, :options, :post, :put, :patch]
        - `:url`      - either full url e.g. "http://example.com/some/path" or just "/some/path" if using `Tesla.Middleware.BaseUrl`
        - `:query`    - a keyword list of query params, e.g. `[page: 1, per_page: 100]`
        - `:headers`  - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
        - `:body`     - depends on used middleware:
            - by default it can be a binary
            - if using e.g. JSON encoding middleware it can be a nested map
            - if adapter supports it it can be a Stream with any of the above
        - `:opts`     - custom, per-request middleware or adapter options

        Examples:

            iex> ExampleApi.request(method: :get, url: "/users/path")

        You can also use shortcut methods like:

            iex> ExampleApi.get("/users/1")

        or

            iex> myclient |> ExampleApi.post("/users", %{name: "Jon"})
        """
        @spec request(Tesla.Env.client, [option]) :: Tesla.Env.t
      else
        @doc false
      end
      def request(client, options) do
        Tesla.perform_request(__MODULE__, client, options)
      end

      if docs do
        @doc """
        Perform a request. See `request/2` for available options.
        """
      @spec request([option]) :: Tesla.Env.t
      else
        @doc false
      end
      def request(options) do
        Tesla.perform_request(__MODULE__, options)
      end

      unquote(generate_http_verbs(opts))

      import Tesla.Builder, only: [plug: 1, plug: 2, adapter: 1, adapter: 2]
      @before_compile Tesla.Builder
    end
  end

  @doc """
  Attach middleware to your API client

  ```ex
  defmodule ExampleApi do
    use Tesla

    # plug middleware module with options
    plug Tesla.Middleware.BaseURL, "http://api.example.com"
    plug Tesla.Middleware.JSON, engine: Poison

    # plug middleware function
    plug :handle_errors

    # middleware function gets two parameters: Tesla.Env and the rest of middleware call stack
    # and must return Tesla.Env
    def handle_errors(env, next) do
      env
      |> modify_env_before_request
      |> Tesla.run(next)            # run the rest of stack
      |> modify_env_after_request
    end
  end
  """
  defmacro plug(middleware, opts \\ nil) do
    opts = Macro.escape(opts)
    middleware = Tesla.alias(middleware)
    quote do: @__middleware__ {unquote(middleware), unquote(opts)}
  end

  @doc """
  Choose adapter for your API client

  ```ex
  defmodule ExampleApi do
    use Tesla

    # set adapter as module
    adapter Tesla.Adapter.Hackney

    # set adapter as function
    adapter :local_adapter

    # set adapter as anonymous function
    adapter fn env ->
      ...
      env
    end

    # adapter function gets Tesla.Env as parameter and must return Tesla.Env
    def local_adapter(env) do
      ...
      env
    end
  end
  """
  defmacro adapter({:fn, _, _} = adapter) do
    adapter = Macro.escape(adapter)
    quote do: @__adapter__ unquote(adapter)
  end
  defmacro adapter(adapter, opts \\ nil) do
    adapter = Tesla.alias(adapter)
    quote do: @__adapter__ {unquote(adapter), unquote(opts)}
  end

  defp generate_http_verbs(opts) do
    selected_verbs =
      @http_verbs
      |> Enum.reject(&(not &1 in Keyword.get(opts, :only, @http_verbs)))
      |> Enum.reject(&(&1 in Keyword.get(opts, :except, [])))

    Enum.map selected_verbs, &generate_api(&1, Keyword.get(opts, :docs, true))
  end

  defp generate_api(method, docs) when method in [:post, :put, :patch] do
    quote do
      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
        """
        @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, Tesla.Env.body, [option]) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(client, url, body, options) when is_function(client) do
        request(client, [method: unquote(method), url: url, body: body] ++ options)
      end

      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
            iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
        """
        @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(client, url, body) when is_function(client) do
        request(client, method: unquote(method), url: url, body: body)
      end
      if unquote(docs) do
        @spec unquote(method)(Tesla.Env.url, Tesla.Env.body, [option]) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(url, body, options) do
        request([method: unquote(method), url: url, body: body] ++ options)
      end

      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
        """
        @spec unquote(method)(Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(url, body) do
        request(method: unquote(method), url: url, body: body)
      end
    end
  end

  defp generate_api(method, docs) when method in [:head, :get, :delete, :trace, :options] do
    quote do
      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> myclient |> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
        """
        @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, [option]) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(client, url, options) when is_function(client) do
        request(client, [method: unquote(method), url: url] ++ options)
      end

      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> myclient |> ExampleApi.#{unquote(method)}("/users")
            iex> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
        """
        @spec unquote(method)(Tesla.Env.client, Tesla.Env.url) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(client, url) when is_function(client) do
        request(client, method: unquote(method), url: url)
      end
      if unquote(docs) do
        @spec unquote(method)(Tesla.Env.url, [option]) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(url, options) do
        request([method: unquote(method), url: url] ++ options)
      end

      if unquote(docs) do
        @doc """
        Perform a #{unquote(method |> to_string |> String.upcase)} request.
        See `request/1` or `request/2` for options definition.

        Example
            iex> ExampleApi.#{unquote(method)}("/users")
        """
        @spec unquote(method)(Tesla.Env.url) :: Tesla.Env.t
      else
        @doc false
      end
      def unquote(method)(url) do
        request(method: unquote(method), url: url)
      end
    end
  end

  defmacro __before_compile__(env) do
    adapter     = Module.get_attribute(env.module, :__adapter__) || quote(do: Tesla.default_adapter)
    middleware  = Module.get_attribute(env.module, :__middleware__) |> Enum.reverse

    quote do
      def __middleware__, do: unquote(middleware)
      def __adapter__, do: unquote(adapter)
    end
  end
end

defmodule Tesla do
  use Tesla.Builder

  @moduledoc """
  A HTTP toolkit for building API clients using middlewares

  Include Tesla module in your api client:

  ```ex
  defmodule ExampleApi do
    use Tesla

    plug Tesla.Middleware.BaseURL, "http://api.example.com"
    plug Tesla.Middleware.JSON
  end
  """

  defmacro __using__(opts \\ []) do
    quote do
      use Tesla.Builder, unquote(opts)
    end
  end

  @aliases [
    httpc:    Tesla.Adapter.Httpc,
    hackney:  Tesla.Adapter.Hackney,
    ibrowse:  Tesla.Adapter.Ibrowse,

    base_url:     Tesla.Middleware.BaseUrl,
    headers:      Tesla.Middleware.Headers,
    query:        Tesla.Middleware.Query,
    decode_rels:  Tesla.Middleware.DecodeRels,
    json:         Tesla.Middleware.JSON,
    logger:       Tesla.Middleware.Logger,
    debug_logger: Tesla.Middleware.DebugLogger
  ]
  def alias(key) when is_atom(key), do: Keyword.get(@aliases, key, key)
  def alias(key), do: key

  def perform_request(module, client \\ nil, options) do
    stack = prepare(module, List.wrap(client) ++ module.__middleware__ ++ default_middleware() ++ [module.__adapter__])
    env   = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
    run(env, stack)
  end

  def prepare(module, stack) do
    Enum.map stack, fn
      {name, opts}              -> prepare_module(module, name, opts)
      name when is_atom(name)   -> prepare_module(module, name, nil)
      fun when is_function(fun) -> {:fn, fun}
    end
  end

  defp prepare_module(module, name, opts) do
    case Atom.to_char_list(name) do
      ~c"Elixir." ++ _ -> {name,   :call, [opts]}
      _                -> {module, name}
    end
  end

  # empty stack case is useful for reusing/testing middlewares (just pass [] as next)
  def run(env, []), do: env

  # last item in stack is adapter - skip passing rest of stack
  def run(env, [{:fn, f}]),  do: apply(f, [env])
  def run(env, [{m,f}]),     do: apply(m, f, [env])
  def run(env, [{m,f,a}]),   do: apply(m, f, [env | a])

  # for all other elements pass (env, next, opts)
  def run(env, [{:fn, f} | rest]),  do: apply(f, [env, rest])
  def run(env, [{m,f} | rest]),     do: apply(m, f, [env, rest])
  def run(env, [{m,f,a} | rest]),   do: apply(m, f, [env, rest | a])

  # useful helper fuctions
  def put_opt(env, key, value) do
    Map.update!(env, :opts, &Keyword.put(&1, key, value))
  end

  def default_adapter do
    adapter = Application.get_env(:tesla, :adapter, :httpc) |> Tesla.alias
    {adapter, []}
  end

  def default_middleware do
    [{Tesla.Middleware.Normalize, nil}]
  end

  @doc """
  Dynamically build client from list of middlewares.

  ```ex
  defmodule ExampleAPI do
    use Tesla

    def new(token) do
      Tesla.build_client([
        {Tesla.Middleware.Headers, %{"Authorization" => token}}
      ])
    end
  end

  client = ExampleAPI.new(token: "abc")
  client |> ExampleAPI.get("/me")
  ```
  """
  defmacro build_client(stack) do
    quote do
      fn env, next -> Tesla.run(env, Tesla.prepare(__MODULE__, unquote(stack)) ++ next) end
    end
  end

  def build_url(url, []), do: url
  def build_url(url, query) do
    join = if String.contains?(url, "?"), do: "&", else: "?"
    url <> join <> encode_query(query)
  end

  defp encode_query(query) do
    query
    |> Enum.flat_map(&encode_pair/1)
    |> URI.encode_query
  end

  defp encode_pair({key, value}) when is_list(value) do
    if Keyword.keyword?(value) do
      Enum.flat_map(value, fn {k,v} -> encode_pair({"#{key}[#{k}]", v}) end)
    else
      Enum.map(value, fn e -> {"#{key}[]", e} end)
    end
  end
  defp encode_pair({key, value}), do: [{key, value}]
end

defmodule Tesla.Error do
  defexception message: "", reason: nil
end

defmodule Tesla.Env do
  @type client      :: (t,stack -> t)
  @type method      :: :head | :get | :delete | :trace | :options | :post | :put | :patch
  @type url         :: binary
  @type param       :: binary | [{(binary | atom), param}]
  @type query       :: [{(binary | atom), param}]
  @type headers     :: %{binary => binary}
  @type body        :: any #
  @type status      :: integer
  @type opts        :: [any]
  @type __module__  :: atom
  @type __client__  :: function

  @type stack       :: [{atom, atom, any}]

  @type t :: %__MODULE__{
            method:     method,
            query:      query,
            url:        url,
            headers:    headers,
            body:       body,
            status:     status,
            opts:       opts,
            __module__: __module__,
            __client__: __client__
  }

  defstruct method:     nil,
            url:        "",
            query:      [],
            headers:    %{},
            body:       nil,
            status:     nil,
            opts:       [],
            __module__: nil,
            __client__: nil
end

defmodule Tesla.Builder do
  @http_verbs ~w(head get delete trace options post put patch)a

  defmacro __using__(opts \\ []) do
    opts = Macro.prewalk(opts, &Macro.expand(&1, __CALLER__))

    quote do
      Module.register_attribute(__MODULE__, :__middleware__, accumulate: true)
      Module.register_attribute(__MODULE__, :__adapter__, [])

      @type option :: {:method,   Tesla.Env.method}  |
                      {:url,      Tesla.Env.url}     |
                      {:query,    Tesla.Env.query}   |
                      {:headers,  Tesla.Env.headers} |
                      {:body,     Tesla.Env.body}    |
                      {:opts,     Tesla.Env.opts}

      @doc """
      Perform a request using client function

      Options:
      - `:method`   - the request method, one of [:head, :get, :delete, :trace, :options, :post, :put, :patch]
      - `:url`      - either full url e.g. "http://example.com/some/path" or just "/some/path" if using `Tesla.Middleware.BaseUrl`
      - `:query`    - a keyword list of query params, e.g. `[page: 1, per_page: 100]`
      - `:headers`  - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
      - `:body`     - depends on used middleware:
          - by default it can be a binary
          - if using e.g. JSON encoding middleware it can be a nested map
          - if adapter supports it it can be a Stream with any of the above
      - `:opts`     - custom, per-request middleware or adapter options

      Examples:

          iex> ExampleApi.request(method: :get, url: "/users/path")

      You can also use shortcut methods like:

          iex> ExampleApi.get("/users/1")

      or

          iex> myclient |> ExampleApi.post("/users", %{name: "Jon"})
      """
      @spec request(Tesla.Env.client, [option]) :: Tesla.Env.t
      def request(client, options) do
        Tesla.perform_request(__MODULE__, client, options)
      end

      @doc """
      Perform a request. See `request/2` for available options.
      """
      @spec request([option]) :: Tesla.Env.t
      def request(options) do
        Tesla.perform_request(__MODULE__, options)
      end

      unquote(generate_http_verbs(opts))

      import Tesla.Builder, only: [plug: 1, plug: 2, adapter: 1, adapter: 2]
      @before_compile Tesla.Builder
    end
  end

  @doc """
  Attach middleware to your API client

  ```ex
  defmodule ExampleApi do
    use Tesla

    # plug middleware module with options
    plug Tesla.Middleware.BaseURL, "http://api.example.com"
    plug Tesla.Middleware.JSON, engine: Poison

    # plug middleware function
    plug :handle_errors

    # middleware function gets two parameters: Tesla.Env and the rest of middleware call stack
    # and must return Tesla.Env
    def handle_errors(env, next) do
      env
      |> modify_env_before_request
      |> Tesla.run(next)            # run the rest of stack
      |> modify_env_after_request
    end
  end
  """
  defmacro plug(middleware, opts \\ nil) do
    opts = Macro.escape(opts)
    middleware = Tesla.alias(middleware)
    quote do: @__middleware__ {unquote(middleware), unquote(opts)}
  end

  @doc """
  Choose adapter for your API client

  ```ex
  defmodule ExampleApi do
    use Tesla

    # set adapter as module
    adapter Tesla.Adapter.Hackney

    # set adapter as function
    adapter :local_adapter

    # set adapter as anonymous function
    adapter fn env ->
      ...
      env
    end

    # adapter function gets Tesla.Env as parameter and must return Tesla.Env
    def local_adapter(env) do
      ...
      env
    end
  end
  """
  defmacro adapter({:fn, _, _} = adapter) do
    adapter = Macro.escape(adapter)
    quote do: @__adapter__ unquote(adapter)
  end
  defmacro adapter(adapter, opts \\ nil) do
    adapter = Tesla.alias(adapter)
    quote do: @__adapter__ {unquote(adapter), unquote(opts)}
  end

  defp generate_http_verbs(opts) do
    selected_verbs =
      @http_verbs
      |> Enum.reject(&(not &1 in Keyword.get(opts, :only, @http_verbs)))
      |> Enum.reject(&(&1 in Keyword.get(opts, :except, [])))

    Enum.map selected_verbs, &generate_api/1
  end

  defp generate_api(method) when method in [:post, :put, :patch] do
    quote do
      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
      """
      @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, Tesla.Env.body, [option]) :: Tesla.Env.t
      def unquote(method)(client, url, body, options) when is_function(client) do
        request(client, [method: unquote(method), url: url, body: body] ++ options)
      end

      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> myclient |> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
          iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"}, query: [scope: "admin"])
      """
      @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
      def unquote(method)(client, url, body) when is_function(client) do
        request(client, method: unquote(method), url: url, body: body)
      end
      @spec unquote(method)(Tesla.Env.url, Tesla.Env.body, [option]) :: Tesla.Env.t
      def unquote(method)(url, body, options) do
        request([method: unquote(method), url: url, body: body] ++ options)
      end

      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> ExampleApi.#{unquote(method)}("/users", %{name: "Jon"})
      """
      @spec unquote(method)(Tesla.Env.url, Tesla.Env.body) :: Tesla.Env.t
      def unquote(method)(url, body) do
        request(method: unquote(method), url: url, body: body)
      end
    end
  end

  defp generate_api(method) when method in [:head, :get, :delete, :trace, :options] do
    quote do
      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> myclient |> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
      """
      @spec unquote(method)(Tesla.Env.client, Tesla.Env.url, [option]) :: Tesla.Env.t
      def unquote(method)(client, url, options) when is_function(client) do
        request(client, [method: unquote(method), url: url] ++ options)
      end

      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> myclient |> ExampleApi.#{unquote(method)}("/users")
          iex> ExampleApi.#{unquote(method)}("/users", query: [page: 1])
      """
      @spec unquote(method)(Tesla.Env.client, Tesla.Env.url) :: Tesla.Env.t
      def unquote(method)(client, url) when is_function(client) do
        request(client, method: unquote(method), url: url)
      end
      @spec unquote(method)(Tesla.Env.url, [option]) :: Tesla.Env.t
      def unquote(method)(url, options) do
        request([method: unquote(method), url: url] ++ options)
      end

      @doc """
      Perform a #{unquote(method |> to_string |> String.upcase)} request.
      See `request/1` or `request/2` for options definition.

      Example
          iex> ExampleApi.#{unquote(method)}("/users")
      """
      @spec unquote(method)(Tesla.Env.url) :: Tesla.Env.t
      def unquote(method)(url) do
        request(method: unquote(method), url: url)
      end
    end
  end

  defmacro __before_compile__(env) do
    adapter     = Module.get_attribute(env.module, :__adapter__) || quote(do: Tesla.default_adapter)
    middleware  = Module.get_attribute(env.module, :__middleware__) |> Enum.reverse

    quote do
      def __middleware__, do: unquote(middleware)
      def __adapter__, do: unquote(adapter)
    end
  end
end

defmodule Tesla do
  use Tesla.Builder

  @moduledoc """
  A HTTP toolkit for building API clients using middlewares

  Include Tesla module in your api client:

  ```ex
  defmodule ExampleApi do
    use Tesla

    plug Tesla.Middleware.BaseURL, "http://api.example.com"
    plug Tesla.Middleware.JSON
  end
  """

  defmacro __using__(opts \\ []) do
    quote do
      use Tesla.Builder, unquote(opts)
    end
  end

  @aliases [
    httpc:    Tesla.Adapter.Httpc,
    hackney:  Tesla.Adapter.Hackney,
    ibrowse:  Tesla.Adapter.Ibrowse,

    base_url:     Tesla.Middleware.BaseUrl,
    headers:      Tesla.Middleware.Headers,
    query:        Tesla.Middleware.Query,
    decode_rels:  Tesla.Middleware.DecodeRels,
    json:         Tesla.Middleware.JSON,
    logger:       Tesla.Middleware.Logger,
    debug_logger: Tesla.Middleware.DebugLogger
  ]
  def alias(key) when is_atom(key), do: Keyword.get(@aliases, key, key)
  def alias(key), do: key

  def perform_request(module, client \\ nil, options) do
    stack = prepare(module, List.wrap(client) ++ module.__middleware__ ++ default_middleware() ++ [module.__adapter__])
    env   = struct(Tesla.Env, options ++ [__module__: module, __client__: client])
    run(env, stack)
  end

  def prepare(module, stack) do
    Enum.map stack, fn
      {name, opts}              -> prepare_module(module, name, opts)
      name when is_atom(name)   -> prepare_module(module, name, nil)
      fun when is_function(fun) -> {:fn, fun}
    end
  end

  defp prepare_module(module, name, opts) do
    case Atom.to_char_list(name) do
      ~c"Elixir." ++ _ -> {name,   :call, [opts]}
      _                -> {module, name}
    end
  end

  # empty stack case is useful for reusing/testing middlewares (just pass [] as next)
  def run(env, []), do: env

  # last item in stack is adapter - skip passing rest of stack
  def run(env, [{:fn, f}]),  do: apply(f, [env])
  def run(env, [{m,f}]),     do: apply(m, f, [env])
  def run(env, [{m,f,a}]),   do: apply(m, f, [env | a])

  # for all other elements pass (env, next, opts)
  def run(env, [{:fn, f} | rest]),  do: apply(f, [env, rest])
  def run(env, [{m,f} | rest]),     do: apply(m, f, [env, rest])
  def run(env, [{m,f,a} | rest]),   do: apply(m, f, [env, rest | a])

  # useful helper fuctions
  def put_opt(env, key, value) do
    Map.update!(env, :opts, &Keyword.put(&1, key, value))
  end

  def default_adapter do
    adapter = Application.get_env(:tesla, :adapter, :httpc) |> Tesla.alias
    {adapter, []}
  end

  def default_middleware do
    [{Tesla.Middleware.Normalize, nil}]
  end

  @doc """
  Dynamically build client from list of middlewares.

  ```ex
  defmodule ExampleAPI do
    use Tesla

    def new(token) do
      Tesla.build_client([
        {Tesla.Middleware.Headers, %{"Authorization" => token}}
      ])
    end
  end

  client = ExampleAPI.new(token: "abc")
  client |> ExampleAPI.get("/me")
  ```
  """
  defmacro build_client(stack) do
    quote do
      fn env, next -> Tesla.run(env, Tesla.prepare(__MODULE__, unquote(stack)) ++ next) end
    end
  end

  def build_url(url, []), do: url
  def build_url(url, query) do
    join = if String.contains?(url, "?"), do: "&", else: "?"
    url <> join <> encode_query(query)
  end

  defp encode_query(query) do
    query
    |> Enum.flat_map(&encode_pair/1)
    |> URI.encode_query
  end

  defp encode_pair({key, value}) when is_list(value) do
    if Keyword.keyword?(value) do
      Enum.flat_map(value, fn {k,v} -> encode_pair({"#{key}[#{k}]", v}) end)
    else
      Enum.map(value, fn e -> {"#{key}[]", e} end)
    end
  end
  defp encode_pair({key, value}), do: [{key, value}]
end


Fix guard clause compiler warning

Fix guard clause compiler warning

defmodule Dogma.ScriptTest do
  use ShouldI

  alias Dogma.Error
  alias Dogma.Script
  alias Dogma.Script.InvalidScriptError

  with "parse/2" do

    with "a valid script" do
      setup context do
        source = """
        defmodule Foo do
          def greet do
            "Hello world!"
          end
        end
        """
        %{
          source: source,
          script: Script.parse( source, "lib/foo.ex" ),
        }
      end

      should "assign path", context do
        assert "lib/foo.ex" == context.script.path
      end

      should "assign source", context do
        assert context.source == context.script.source
      end

      should "assign processed source", context do
        processed_source = """
        defmodule Foo do
          def greet do
            ""
          end
        end
        """
        assert processed_source == context.script.processed_source
      end

      should "assign an empty list of errors", context do
        assert [] == context.script.errors
      end

      should "assigns lines", context do
        lines = [
          {1,  ~s(defmodule Foo do)},
          {2,  ~s(  def greet do)},
          {3,  ~s(    "Hello world!")},
          {4,  ~s(  end)},
          {5,  ~s(end)},
        ]
        assert lines == context.script.lines
      end

      should "assigns processed lines", context do
        lines = [
          {1,  ~s(defmodule Foo do)},
          {2,  ~s(  def greet do)},
          {3,  ~s(    "")},
          {4,  ~s(  end)},
          {5,  ~s(end)},
        ]
        assert lines == context.script.processed_lines
      end

      should "assign valid? as true", context do
        assert context.script.valid?
      end

      should "assigns the quoted abstract syntax tree", context do
        {:ok, ast} = Code.string_to_quoted( context.source )
        assert ast == context.script.ast
      end

      should "include line numbers in the quoted ast" do
        script = Script.parse( "1 + 1", "" )
        assert {:+, [line: 1], [1, 1]} == script.ast
      end

      should "assigns the tokenized source", context do
        case context.script.tokens do
          [{:identifier, _, :defmodule}, {:aliases, _, [:Foo]},
                  {:do, _}, {:eol, _}, {:identifier, _, :def},
                  {:do_identifier, _, :greet}, {:do, _}, {:eol, _},
                  {:bin_string, _, ["Hello world!"]}, {:eol, _}, {:end, _},
                  {:eol, _}, {:end, _}, {:eol, _}]
            -> assert 1 == 1
          _ -> assert false
        end
      end
    end

    with "an invalid script" do
      setup context do
        source = ~s"""
        <>>>>>>><><>><><>>>>>>>>>>>>>><<><
        """
        %{
          script: Script.parse( source, "" ),
        }
      end

      should "assign valid? as false", context do
        refute context.script.valid?
      end

      should "assign [] in place of AST", context do
        assert [] == context.script.ast
      end

      should "assign [] in place of tokens", context do
        assert [] == context.script.ast
      end

      should "assign a syntax error", context do
        error = %Error{
          rule: SyntaxError,
          message: ~s[missing terminator: >> (for "<<" starting at line 1)],
          line: 1,
        }
        assert [error] == context.script.errors
      end
    end

    with "a script with trailing blank lines" do
      setup context do
        source = """
        1 + 2

        """
        %{
          source: source,
          script: Script.parse( source, "lib/foo.ex" ),
        }
      end

      should "preserve the extra blank lines", context do
        lines = [
          {1, "1 + 2"},
          {2, ""},
          {3, ""},
        ]
        assert lines == context.script.lines
      end
    end
  end

  with "parse!/2" do
    should "raise InvalidScriptError with an invalid script" do
      assert_raise InvalidScriptError, fn ->
        "<>>>>>>><><>><><>>>>>>>>>>>>>><<><" |> Script.parse!( "foo.ex" )
      end
    end

    should "be identical to parse/2 for valid scripts" do
      source = """
      defmodule Foo do
        def greet do
          "Hello world!"
        end
      end
      """
      expected = source |> Script.parse( "foo.ex" )
      actual   = source |> Script.parse!( "foo.ex" )
      assert expected == actual
    end
  end

  with "walk/2" do
    setup context do
      %{
        script: Script.parse( "2 * 3 + 1", "foo.ex" )
      }
    end

    should "run the fn on each node, and return the accumulator", context do
      fun    = fn(node, errors) -> {node, [node | errors]} end
      walked = Script.walk( context.script, fun )
      nodes_walked = [
        1,
        3,
        2,
        {:*, [line: 1], [2, 3]},
        {:+, [line: 1], [{:*, [line: 1], [2, 3]}, 1]}
      ]
      assert nodes_walked == walked
    end

    should "allow you to skip nodes", context do
      fun    = fn(node, errors) -> {[], [node | errors]} end
      walked = Script.walk( context.script, fun )
      nodes_walked = [
        {:+, [line: 1], [{:*, [line: 1], [2, 3]}, 1]}
      ]
      assert nodes_walked == walked
    end
  end

  with "run_tests/1" do

    setup context do
      %{
        script: Script.parse( "1 + 1", "foo.ex" ),
        rules: [
          {TestRules.TestOne},
          {TestRules.TestTwo, output: "hello world"}
        ],
      }
    end

    should "run the given rules", context do
      errors = context.script |> Script.run_tests(context.rules)
      assert [1, "hello world"] == errors
    end
  end
end

defmodule Dogma.Rule.TestRules do
  defmodule TestOne do
    def test(_) do
      [1]
    end
  end

  defmodule TestTwo do
    def test(_, output: custom_out) do
      [custom_out]
    end
  end
end

defmodule Dogma.ScriptTest do
  use ShouldI

  alias Dogma.Error
  alias Dogma.Script
  alias Dogma.Script.InvalidScriptError

  with "parse/2" do

    with "a valid script" do
      setup context do
        source = """
        defmodule Foo do
          def greet do
            "Hello world!"
          end
        end
        """
        %{
          source: source,
          script: Script.parse( source, "lib/foo.ex" ),
        }
      end

      should "assign path", context do
        assert "lib/foo.ex" == context.script.path
      end

      should "assign source", context do
        assert context.source == context.script.source
      end

      should "assign processed source", context do
        processed_source = """
        defmodule Foo do
          def greet do
            ""
          end
        end
        """
        assert processed_source == context.script.processed_source
      end

      should "assign an empty list of errors", context do
        assert [] == context.script.errors
      end

      should "assigns lines", context do
        lines = [
          {1,  ~s(defmodule Foo do)},
          {2,  ~s(  def greet do)},
          {3,  ~s(    "Hello world!")},
          {4,  ~s(  end)},
          {5,  ~s(end)},
        ]
        assert lines == context.script.lines
      end

      should "assigns processed lines", context do
        lines = [
          {1,  ~s(defmodule Foo do)},
          {2,  ~s(  def greet do)},
          {3,  ~s(    "")},
          {4,  ~s(  end)},
          {5,  ~s(end)},
        ]
        assert lines == context.script.processed_lines
      end

      should "assign valid? as true", context do
        assert context.script.valid?
      end

      should "assigns the quoted abstract syntax tree", context do
        {:ok, ast} = Code.string_to_quoted( context.source )
        assert ast == context.script.ast
      end

      should "include line numbers in the quoted ast" do
        script = Script.parse( "1 + 1", "" )
        assert {:+, [line: 1], [1, 1]} == script.ast
      end

      should "assigns the tokenized source", context do
        case context.script.tokens do
          [{:identifier, _, :defmodule}, {:aliases, _, [:Foo]},
                  {:do, _}, {:eol, _}, {:identifier, _, :def},
                  {:do_identifier, _, :greet}, {:do, _}, {:eol, _},
                  {:bin_string, _, ["Hello world!"]}, {:eol, _}, {:end, _},
                  {:eol, _}, {:end, _}, {:eol, _}]
            -> assert true
          _ -> assert false
        end
      end
    end

    with "an invalid script" do
      setup context do
        source = ~s"""
        <>>>>>>><><>><><>>>>>>>>>>>>>><<><
        """
        %{
          script: Script.parse( source, "" ),
        }
      end

      should "assign valid? as false", context do
        refute context.script.valid?
      end

      should "assign [] in place of AST", context do
        assert [] == context.script.ast
      end

      should "assign [] in place of tokens", context do
        assert [] == context.script.ast
      end

      should "assign a syntax error", context do
        error = %Error{
          rule: SyntaxError,
          message: ~s[missing terminator: >> (for "<<" starting at line 1)],
          line: 1,
        }
        assert [error] == context.script.errors
      end
    end

    with "a script with trailing blank lines" do
      setup context do
        source = """
        1 + 2

        """
        %{
          source: source,
          script: Script.parse( source, "lib/foo.ex" ),
        }
      end

      should "preserve the extra blank lines", context do
        lines = [
          {1, "1 + 2"},
          {2, ""},
          {3, ""},
        ]
        assert lines == context.script.lines
      end
    end
  end

  with "parse!/2" do
    should "raise InvalidScriptError with an invalid script" do
      assert_raise InvalidScriptError, fn ->
        "<>>>>>>><><>><><>>>>>>>>>>>>>><<><" |> Script.parse!( "foo.ex" )
      end
    end

    should "be identical to parse/2 for valid scripts" do
      source = """
      defmodule Foo do
        def greet do
          "Hello world!"
        end
      end
      """
      expected = source |> Script.parse( "foo.ex" )
      actual   = source |> Script.parse!( "foo.ex" )
      assert expected == actual
    end
  end

  with "walk/2" do
    setup context do
      %{
        script: Script.parse( "2 * 3 + 1", "foo.ex" )
      }
    end

    should "run the fn on each node, and return the accumulator", context do
      fun    = fn(node, errors) -> {node, [node | errors]} end
      walked = Script.walk( context.script, fun )
      nodes_walked = [
        1,
        3,
        2,
        {:*, [line: 1], [2, 3]},
        {:+, [line: 1], [{:*, [line: 1], [2, 3]}, 1]}
      ]
      assert nodes_walked == walked
    end

    should "allow you to skip nodes", context do
      fun    = fn(node, errors) -> {[], [node | errors]} end
      walked = Script.walk( context.script, fun )
      nodes_walked = [
        {:+, [line: 1], [{:*, [line: 1], [2, 3]}, 1]}
      ]
      assert nodes_walked == walked
    end
  end

  with "run_tests/1" do

    setup context do
      %{
        script: Script.parse( "1 + 1", "foo.ex" ),
        rules: [
          {TestRules.TestOne},
          {TestRules.TestTwo, output: "hello world"}
        ],
      }
    end

    should "run the given rules", context do
      errors = context.script |> Script.run_tests(context.rules)
      assert [1, "hello world"] == errors
    end
  end
end

defmodule Dogma.Rule.TestRules do
  defmodule TestOne do
    def test(_) do
      [1]
    end
  end

  defmodule TestTwo do
    def test(_, output: custom_out) do
      [custom_out]
    end
  end
end