Fix compiler warning on Elixir 1.4

Fix compiler warning on Elixir 1.4

Dict.merge calls now emit deprecation warnings.

defmodule Kazan.Codegen.Apis do
  @moduledoc false
  # Macros for generating API clients from OAI specs.
  import Kazan.Swagger, only: [swagger_to_op_map: 1]
  alias Kazan.Codegen.Apis.{Operation, Parameter}

  require EEx

  @doc """
  Generates API client modules for all the operations defined in an OAPI spec.

  This reads the provided file at compile time and uses it to generate functions
  and their corresponding models.

  The modules will be defined by the tags for each of the operations, and the
  functions will be named using a camel case version of the operationId.

  Currently the operationId has some tag-related data embedded in it, which we
  remove for the sake of brevity.
  """
  defmacro from_spec(spec_file) do
    api_groups =
      File.read!(spec_file)
      |> Poison.decode!
      |> swagger_to_op_map
      |> Map.values
      |> Enum.flat_map(&duplicate_on_tags/1)
      |> Enum.map(&Operation.from_oai_desc/1)
      |> Enum.group_by(fn (op) -> op.api_name end)

    module_forms = for {module_name, functions} <- api_groups do
      function_forms = Enum.map(functions, &function_form/1)
      module_doc = module_doc(module_name)

      quote do
        defmodule unquote(module_name) do
          @moduledoc unquote(module_doc)

          unquote_splicing(function_forms)
        end
      end
    end

    quote do
      @external_resource unquote(spec_file)

      unquote_splicing(module_forms)
    end
  end

  @doc """
  Builds an api_name from a tag on an OAI operation.
  """
  @spec api_name(String.t) :: atom
  def api_name(operation_tag) do
    api_name = Macro.camelize(operation_tag)
    Module.concat(Kazan.Apis, api_name)
  end

  @doc """
  Builds a function name from the operationId of an OAI operation.

  We take the operation tag in here too, because the Kube OAI operations use IDs
  like listCoreV1ConfigMapForAllNamespaces where the operation is on the core_v1
  API. We don't want to have such large function names, so we try to strip the
  API name out.
  """
  @spec function_name(String.t, String.t | atom) :: atom
  def function_name(operation_id, tag) when is_binary(tag) do
    operation_id
    |> String.replace(Macro.camelize(tag), "")
    |> Macro.underscore
    |> String.to_atom
  end

  # Swagger tags are a list. There _appears_ to only be one tag per operation,
  # but there could be more.  We handle that by duplicating on tags.
  # Once this function is finished, we will have a bunch of operations with a
  # single tag.
  @spec duplicate_on_tags(Map.t) :: [Map.t]
  defp duplicate_on_tags(operation) do
    for tag <- operation["tags"] do
      operation |> Map.put("tag", tag) |> Map.delete("tags")
    end
  end

  # Builds the quoted function form for an operation function.
  @spec function_form(Operation.t) :: term
  defp function_form(operation) do
    param_groups = Enum.group_by(
      operation.parameters,
      fn (param) -> param.type end
    )

    is_required = fn (param) -> param.required end
    query_params = Map.get(param_groups, :query, [])

    path_params =
      param_groups |> Map.get(:path, []) |> sort_path_params(operation.path)

    # The main arguments our function will take:
    argument_params =
      Map.get(param_groups, :body, [])
      ++ path_params
      ++ Enum.filter(query_params, is_required)

    optional_params = Enum.reject(query_params, is_required)

    arguments = argument_forms(argument_params, optional_params)
    docs = function_docs(
      operation.description, argument_params, optional_params,
      operation.response_schema
    )

    param_unpacking = if Enum.empty?(argument_params) do
      quote do
        %{}
      end
    else
      argument_map_pairs = for arg <- argument_params do
        {arg.var_name, Macro.var(arg.var_name, __MODULE__)}
      end
      quote location: :keep do
        %{unquote_splicing(argument_map_pairs)}
      end
    end

    option_merging = cond do
      Enum.empty?(optional_params) ->
        quote do
        end
      Enum.empty?(argument_params) ->
        quote location: :keep do
          Enum.into(options, %{})
        end
      :otherwise ->
        quote location: :keep do
          Map.merge(Enum.into(options, %{}), params)
        end
    end

    transform_map = for parameter <- operation.parameters, into: %{} do
      {parameter.var_name, parameter.field_name}
    end

    bang_function_name = String.to_atom(
      Atom.to_string(operation.function_name) <> "!"
    )
    argument_forms_in_call = argument_call_forms(
      argument_params, optional_params
    )

    quote location: :keep do
      @doc unquote(docs)
      def unquote(operation.function_name)(unquote_splicing(arguments)) do
        params = unquote(param_unpacking)
        params = unquote(option_merging)
        {:ok, req} = Kazan.Request.create(
          unquote(operation.operation_id),
          Kazan.Codegen.Apis.transform_request_parameters(
            unquote(Macro.escape(transform_map)),
            params
          )
        )
      end

      @doc unquote(docs)
      def unquote(bang_function_name)(unquote_splicing(arguments)) do
        rv = unquote(operation.function_name)(
          unquote_splicing(argument_forms_in_call)
        )
        case rv do
          {:ok, result} -> result
          {:err, reason} ->
            raise Kazan.BuildRequestError,
              reason: reason, operation: unquote(operation.function_name)
        end
      end
    end
  end

  # Transforms a map of function arguments into a map of request parameters.
  def transform_request_parameters(parameter_descs, parameters) do
    for {k, v} <- parameters, into: %{} do
      {parameter_descs[k], v}
    end
  end

  # List of argument forms to go in function argument lists.
  @spec argument_forms([Map.t], [Map.t]) :: [term]
  defp argument_forms(argument_params, []) do
    for param <- argument_params do
      Macro.var(param.var_name, __MODULE__)
    end
  end
  defp argument_forms(argument_params, _optional_params) do
    argument_forms(argument_params, [])
    ++ [{:\\, [], [Macro.var(:options, __MODULE__), []]}]
  end

  # List of arugment forms to go in call to function from bang function.
  @spec argument_call_forms([Map.t], [Map.t]) :: [term]
  defp argument_call_forms(argument_params, []) do
    for param <- argument_params do
      Macro.var(param.var_name, __MODULE__)
    end
  end
  defp argument_call_forms(argument_params, _optional_params) do
    argument_forms(argument_params, [])
    ++ [Macro.var(:options, __MODULE__)]
  end

  # The Kube API specs provide path parameters in an unintuitive order,
  # so we sort them by the order they appear in the request path here.
  @spec sort_path_params([Parameter.t], String.t) :: [Parameter.t]
  defp sort_path_params(parameters, path) do
    Enum.sort(parameters, fn (param1, param2) ->
      loc1 = str_index("{#{param1.field_name}}", path)
      loc2 = str_index("{#{param2.field_name}}", path)
      loc1 <= loc2
    end)
  end

  # I can't believe I'm having to implement this myself :/
  defp str_index(needle, haystack) do
    case String.split(haystack, needle, parts: 2) do
      [left, _] -> String.length(left)
      [_] -> nil
    end
  end

  EEx.function_from_string(:defp, :function_docs, """
  <%= if description do description end %>

  <%= unless Enum.empty?(parameters) do %>
  ### Parameters

  <%= for param <- parameters do %>
  * `<%= param.var_name %>` - <%= param.description %><%= if param.schema do %>See `<%= doc_ref(param.schema) %>`. <% end %> <% end %>
  <% end %>

  <%= unless Enum.empty?(options) do %>

  ### Options

  <%= for option <- options do %>
  * `<%= option.var_name %>` - <%= option.description %>
  <% end %>
  <% end %>

  <%= if response_schema do %>
  ### Response

  See `<%= doc_ref(response_schema) %>`
  <% end %>

  """, [:description, :parameters, :options, :response_schema])

  defp module_doc(module_name) do
    module_name =
      module_name |> Atom.to_string |> String.split(".") |> List.last
    """
    Contains functions for the #{module_name} API.

    Each of these functions will output a Kazan.Request suitable for passing to
    Kazan.Client.
    """
  end

  # Strips the `Elixir.` prefix from an atom for use in documentation.
  # Atoms will not be linked if they include the Elixir. prefix.
  defp doc_ref(str) do
    str |> Atom.to_string |> String.replace(~r/^Elixir./, "")
  end
end

defmodule Kazan.Codegen.Apis do
  @moduledoc false
  # Macros for generating API clients from OAI specs.
  import Kazan.Swagger, only: [swagger_to_op_map: 1]
  alias Kazan.Codegen.Apis.{Operation, Parameter}

  require EEx

  @doc """
  Generates API client modules for all the operations defined in an OAPI spec.

  This reads the provided file at compile time and uses it to generate functions
  and their corresponding models.

  The modules will be defined by the tags for each of the operations, and the
  functions will be named using a camel case version of the operationId.

  Currently the operationId has some tag-related data embedded in it, which we
  remove for the sake of brevity.
  """
  defmacro from_spec(spec_file) do
    api_groups =
      File.read!(spec_file)
      |> Poison.decode!
      |> swagger_to_op_map
      |> Map.values
      |> Enum.flat_map(&duplicate_on_tags/1)
      |> Enum.map(&Operation.from_oai_desc/1)
      |> Enum.group_by(fn (op) -> op.api_name end)

    module_forms = for {module_name, functions} <- api_groups do
      function_forms = Enum.map(functions, &function_form/1)
      module_doc = module_doc(module_name)

      quote do
        defmodule unquote(module_name) do
          @moduledoc unquote(module_doc)

          unquote_splicing(function_forms)
        end
      end
    end

    quote do
      @external_resource unquote(spec_file)

      unquote_splicing(module_forms)
    end
  end

  @doc """
  Builds an api_name from a tag on an OAI operation.
  """
  @spec api_name(String.t) :: atom
  def api_name(operation_tag) do
    api_name = Macro.camelize(operation_tag)
    Module.concat(Kazan.Apis, api_name)
  end

  @doc """
  Builds a function name from the operationId of an OAI operation.

  We take the operation tag in here too, because the Kube OAI operations use IDs
  like listCoreV1ConfigMapForAllNamespaces where the operation is on the core_v1
  API. We don't want to have such large function names, so we try to strip the
  API name out.
  """
  @spec function_name(String.t, String.t | atom) :: atom
  def function_name(operation_id, tag) when is_binary(tag) do
    operation_id
    |> String.replace(Macro.camelize(tag), "")
    |> Macro.underscore
    |> String.to_atom
  end

  # Swagger tags are a list. There _appears_ to only be one tag per operation,
  # but there could be more.  We handle that by duplicating on tags.
  # Once this function is finished, we will have a bunch of operations with a
  # single tag.
  @spec duplicate_on_tags(Map.t) :: [Map.t]
  defp duplicate_on_tags(operation) do
    for tag <- operation["tags"] do
      operation |> Map.put("tag", tag) |> Map.delete("tags")
    end
  end

  # Builds the quoted function form for an operation function.
  @spec function_form(Operation.t) :: term
  defp function_form(operation) do
    param_groups = Enum.group_by(
      operation.parameters,
      fn (param) -> param.type end
    )

    is_required = fn (param) -> param.required end
    query_params = Map.get(param_groups, :query, [])

    path_params =
      param_groups |> Map.get(:path, []) |> sort_path_params(operation.path)

    # The main arguments our function will take:
    argument_params =
      Map.get(param_groups, :body, [])
      ++ path_params
      ++ Enum.filter(query_params, is_required)

    optional_params = Enum.reject(query_params, is_required)

    arguments = argument_forms(argument_params, optional_params)
    docs = function_docs(
      operation.description, argument_params, optional_params,
      operation.response_schema
    )

    param_unpacking = if Enum.empty?(argument_params) do
      quote do
        %{}
      end
    else
      argument_map_pairs = for arg <- argument_params do
        {arg.var_name, Macro.var(arg.var_name, __MODULE__)}
      end
      quote location: :keep do
        %{unquote_splicing(argument_map_pairs)}
      end
    end

    option_merging = cond do
      Enum.empty?(optional_params) ->
        quote do
        end
      Enum.empty?(argument_params) ->
        quote location: :keep do
          Enum.into(options, %{})
        end
      :otherwise ->
        quote location: :keep do
          Dict.merge(Enum.into(options, %{}), params)
        end
    end

    transform_map = for parameter <- operation.parameters, into: %{} do
      {parameter.var_name, parameter.field_name}
    end

    bang_function_name = String.to_atom(
      Atom.to_string(operation.function_name) <> "!"
    )
    argument_forms_in_call = argument_call_forms(
      argument_params, optional_params
    )

    quote location: :keep do
      @doc unquote(docs)
      def unquote(operation.function_name)(unquote_splicing(arguments)) do
        params = unquote(param_unpacking)
        params = unquote(option_merging)
        {:ok, req} = Kazan.Request.create(
          unquote(operation.operation_id),
          Kazan.Codegen.Apis.transform_request_parameters(
            unquote(Macro.escape(transform_map)),
            params
          )
        )
      end

      @doc unquote(docs)
      def unquote(bang_function_name)(unquote_splicing(arguments)) do
        rv = unquote(operation.function_name)(
          unquote_splicing(argument_forms_in_call)
        )
        case rv do
          {:ok, result} -> result
          {:err, reason} ->
            raise Kazan.BuildRequestError,
              reason: reason, operation: unquote(operation.function_name)
        end
      end
    end
  end

  # Transforms a map of function arguments into a map of request parameters.
  def transform_request_parameters(parameter_descs, parameters) do
    for {k, v} <- parameters, into: %{} do
      {parameter_descs[k], v}
    end
  end

  # List of argument forms to go in function argument lists.
  @spec argument_forms([Map.t], [Map.t]) :: [term]
  defp argument_forms(argument_params, []) do
    for param <- argument_params do
      Macro.var(param.var_name, __MODULE__)
    end
  end
  defp argument_forms(argument_params, _optional_params) do
    argument_forms(argument_params, [])
    ++ [{:\\, [], [Macro.var(:options, __MODULE__), []]}]
  end

  # List of arugment forms to go in call to function from bang function.
  @spec argument_call_forms([Map.t], [Map.t]) :: [term]
  defp argument_call_forms(argument_params, []) do
    for param <- argument_params do
      Macro.var(param.var_name, __MODULE__)
    end
  end
  defp argument_call_forms(argument_params, _optional_params) do
    argument_forms(argument_params, [])
    ++ [Macro.var(:options, __MODULE__)]
  end

  # The Kube API specs provide path parameters in an unintuitive order,
  # so we sort them by the order they appear in the request path here.
  @spec sort_path_params([Parameter.t], String.t) :: [Parameter.t]
  defp sort_path_params(parameters, path) do
    Enum.sort(parameters, fn (param1, param2) ->
      loc1 = str_index("{#{param1.field_name}}", path)
      loc2 = str_index("{#{param2.field_name}}", path)
      loc1 <= loc2
    end)
  end

  # I can't believe I'm having to implement this myself :/
  defp str_index(needle, haystack) do
    case String.split(haystack, needle, parts: 2) do
      [left, _] -> String.length(left)
      [_] -> nil
    end
  end

  EEx.function_from_string(:defp, :function_docs, """
  <%= if description do description end %>

  <%= unless Enum.empty?(parameters) do %>
  ### Parameters

  <%= for param <- parameters do %>
  * `<%= param.var_name %>` - <%= param.description %><%= if param.schema do %>See `<%= doc_ref(param.schema) %>`. <% end %> <% end %>
  <% end %>

  <%= unless Enum.empty?(options) do %>

  ### Options

  <%= for option <- options do %>
  * `<%= option.var_name %>` - <%= option.description %>
  <% end %>
  <% end %>

  <%= if response_schema do %>
  ### Response

  See `<%= doc_ref(response_schema) %>`
  <% end %>

  """, [:description, :parameters, :options, :response_schema])

  defp module_doc(module_name) do
    module_name =
      module_name |> Atom.to_string |> String.split(".") |> List.last
    """
    Contains functions for the #{module_name} API.

    Each of these functions will output a Kazan.Request suitable for passing to
    Kazan.Client.
    """
  end

  # Strips the `Elixir.` prefix from an atom for use in documentation.
  # Atoms will not be linked if they include the Elixir. prefix.
  defp doc_ref(str) do
    str |> Atom.to_string |> String.replace(~r/^Elixir./, "")
  end
end


Rename function. format_result() -> inspirational_quote()

defmodule Functions do
  use Koans

  @intro "Functions"

  def greet(name) do
    "Hello, #{name}!"
  end

  koan "Functions map arguments to outputs" do
    assert greet("World") == ___
  end

  def multiply(a, b), do: a * b

  koan "Single line functions are cool, but mind the comma and the colon!" do
    assert 6 == multiply(2, ___)
  end

  def first(foo, bar), do: "#{foo} and #{bar}"
  def first(foo), do: "Only #{foo}"

  koan "Functions with the same name are distinguished by the number of arguments they take" do
    assert first("One", "Two") == ___
    assert first("One") == ___
  end

  def repeat_again(message, times \\ 5) do
    String.duplicate(message, times)
  end

  koan "Functions can have default argument values" do
    assert repeat_again("Hello ") == ___
    assert repeat_again("Hello ", 2) == ___
  end

  def sum_up(thing) when is_list(thing), do: :entire_list
  def sum_up(_thing), do: :single_thing

  koan "Functions can have guard expressions" do
    assert sum_up([1, 2, 3]) == ___
    assert sum_up(1) == ___
  end

  def bigger(a, b) when a > b, do: "#{a} is bigger than #{b}"
  def bigger(a, b) when a <= b, do: "#{a} is not bigger than #{b}"

  koan "Intricate guards are possible, but be mindful of the reader" do
    assert bigger(10, 5) == ___
    assert bigger(4, 27) == ___
  end

  def get_number(0), do: "The number was zero"
  def get_number(number), do: "The number was #{number}"

  koan "For simpler cases, pattern matching is effective" do
    assert get_number(0) == ___
    assert get_number(5) == ___
  end

  koan "Little anonymous functions are common, and called with a dot" do
    multiply = fn a, b -> a * b end
    assert multiply.(2, 3) == ___
  end

  koan "You can even go shorter, by using capture syntax `&()` and positional arguments" do
    multiply = &(&1 * &2)
    assert multiply.(2, 3) == ___
  end

  koan "Prefix a string with & to build a simple anonymous greet function" do
    greet = &"Hi, #{&1}!"
    assert greet.("Foo") == ___
  end

  koan "You can build anonymous functions out of any elixir expression by prefixing it with &" do
    three_times = &[&1, &1, &1]
    assert three_times.("foo") == ___
  end

  koan "You can use pattern matching to define multiple cases for anonymous functions" do
    inspirational_quote = fn
      {:ok, result} -> "Success is #{result}"
      {:error, reason} -> "You just lost #{reason}"
    end

    assert inspirational_quote.({:ok, "no accident"}) == ___
    assert inspirational_quote.({:error, "the game"}) == ___
  end

  def times_five_and_then(number, fun), do: fun.(number * 5)
  def square(number), do: number * number

  koan "You can pass functions around as arguments. Place an '&' before the name and state the arity" do
    assert times_five_and_then(2, &square/1) == ___
  end

  koan "The '&' operation is not needed for anonymous functions" do
    cube = fn number -> number * number * number end
    assert times_five_and_then(2, cube) == ___
  end

  koan "The result of a function can be piped into another function as its first argument" do
    result =
      "full-name"
      |> String.split("-")
      |> Enum.map(&String.capitalize/1)
      |> Enum.join(" ")

    assert result == ___
  end

  koan "Conveniently keyword lists can be used for function options" do
    transform = fn str, opts ->
      if opts[:upcase] do
        String.upcase(str)
      else
        str
      end
    end

    assert transform.("good", upcase: true) == ___
    assert transform.("good", upcase: false) == ___
  end
end

defmodule Functions do
  use Koans

  @intro "Functions"

  def greet(name) do
    "Hello, #{name}!"
  end

  koan "Functions map arguments to outputs" do
    assert greet("World") == ___
  end

  def multiply(a, b), do: a * b

  koan "Single line functions are cool, but mind the comma and the colon!" do
    assert 6 == multiply(2, ___)
  end

  def first(foo, bar), do: "#{foo} and #{bar}"
  def first(foo), do: "Only #{foo}"

  koan "Functions with the same name are distinguished by the number of arguments they take" do
    assert first("One", "Two") == ___
    assert first("One") == ___
  end

  def repeat_again(message, times \\ 5) do
    String.duplicate(message, times)
  end

  koan "Functions can have default argument values" do
    assert repeat_again("Hello ") == ___
    assert repeat_again("Hello ", 2) == ___
  end

  def sum_up(thing) when is_list(thing), do: :entire_list
  def sum_up(_thing), do: :single_thing

  koan "Functions can have guard expressions" do
    assert sum_up([1, 2, 3]) == ___
    assert sum_up(1) == ___
  end

  def bigger(a, b) when a > b, do: "#{a} is bigger than #{b}"
  def bigger(a, b) when a <= b, do: "#{a} is not bigger than #{b}"

  koan "Intricate guards are possible, but be mindful of the reader" do
    assert bigger(10, 5) == ___
    assert bigger(4, 27) == ___
  end

  def get_number(0), do: "The number was zero"
  def get_number(number), do: "The number was #{number}"

  koan "For simpler cases, pattern matching is effective" do
    assert get_number(0) == ___
    assert get_number(5) == ___
  end

  koan "Little anonymous functions are common, and called with a dot" do
    multiply = fn a, b -> a * b end
    assert multiply.(2, 3) == ___
  end

  koan "You can even go shorter, by using capture syntax `&()` and positional arguments" do
    multiply = &(&1 * &2)
    assert multiply.(2, 3) == ___
  end

  koan "Prefix a string with & to build a simple anonymous greet function" do
    greet = &"Hi, #{&1}!"
    assert greet.("Foo") == ___
  end

  koan "You can build anonymous functions out of any elixir expression by prefixing it with &" do
    three_times = &[&1, &1, &1]
    assert three_times.("foo") == ___
  end

  koan "You can use pattern matching to define multiple cases for anonymous functions" do
    format_result = fn
      {:ok, result} -> "Success is #{result}"
      {:error, reason} -> "You just lost #{reason}"
    end

    assert format_result.({:ok, "no accident"}) == ___
    assert format_result.({:error, "the game"}) == ___
  end

  def times_five_and_then(number, fun), do: fun.(number * 5)
  def square(number), do: number * number

  koan "You can pass functions around as arguments. Place an '&' before the name and state the arity" do
    assert times_five_and_then(2, &square/1) == ___
  end

  koan "The '&' operation is not needed for anonymous functions" do
    cube = fn number -> number * number * number end
    assert times_five_and_then(2, cube) == ___
  end

  koan "The result of a function can be piped into another function as its first argument" do
    result =
      "full-name"
      |> String.split("-")
      |> Enum.map(&String.capitalize/1)
      |> Enum.join(" ")

    assert result == ___
  end

  koan "Conveniently keyword lists can be used for function options" do
    transform = fn str, opts ->
      if opts[:upcase] do
        String.upcase(str)
      else
        str
      end
    end

    assert transform.("good", upcase: true) == ___
    assert transform.("good", upcase: false) == ___
  end
end


Fix typo

Fix typo

defmodule Protocols do
  use Koans

  @intro "Want to follow the rules? Adhere to the protocol!"

  defprotocol School, do: def enrol(person)

  defimpl School, for: Any do
    def enrol(_) do
      "Pupil enrolled at school"
    end
  end

  defmodule Student do
    @derive School
    defstruct name: ""
  end

  defmodule Musician, do: defstruct name: "", instrument: ""
  defmodule Dancer,  do: defstruct name: "", dance_style: ""
  defmodule Baker, do: defstruct name: ""

  defimpl School, for: Musician do
    def enrol(musician) do
      "#{musician.name} signed up for #{musician.instrument}"
    end
  end

  defimpl School, for: Dancer do
    def enrol(dancer), do: "#{dancer.name} enrolled for #{dancer.dance_style}"
  end

  koan "Sharing an interface is the secret at school" do
    musician = %Musician{name: "Andre", instrument: "violin"}
    dancer = %Dancer{name: "Darcy", dance_style: "ballet"}

    assert School.enrol(musician) == ___
    assert School.enrol(dancer) == ___
  end

  koan "Sometimes we all use the same" do
    student = %Student{name: "Emily"}
    assert School.enrol(student) == ___
  end

  koan "If you don't comply you can't get in" do
    assert_raise ___, fn ->
      School.enrol(%Baker{name: "Delia"})
    end
  end
end

defmodule Protocols do
  use Koans

  @intro "Wan't to follow the rules? Adhere to the protocol!"

  defprotocol School, do: def enrol(person)

  defimpl School, for: Any do
    def enrol(_) do
      "Pupil enrolled at school"
    end
  end

  defmodule Student do
    @derive School
    defstruct name: ""
  end

  defmodule Musician, do: defstruct name: "", instrument: ""
  defmodule Dancer,  do: defstruct name: "", dance_style: ""
  defmodule Baker, do: defstruct name: ""

  defimpl School, for: Musician do
    def enrol(musician) do
      "#{musician.name} signed up for #{musician.instrument}"
    end
  end

  defimpl School, for: Dancer do
    def enrol(dancer), do: "#{dancer.name} enrolled for #{dancer.dance_style}"
  end

  koan "Sharing an interface is the secret at school" do
    musician = %Musician{name: "Andre", instrument: "violin"}
    dancer = %Dancer{name: "Darcy", dance_style: "ballet"}

    assert School.enrol(musician) == ___
    assert School.enrol(dancer) == ___
  end

  koan "Sometimes we all use the same" do
    student = %Student{name: "Emily"}
    assert School.enrol(student) == ___
  end

  koan "If you don't comply you can't get in" do
    assert_raise ___, fn ->
      School.enrol(%Baker{name: "Delia"})
    end
  end
end


fix enum_to_spec warning

fix enum_to_spec warning

defmodule Protobuf.TypeUtil do
  def from_enum(:TYPE_DOUBLE), do: :double
  def from_enum(:TYPE_FLOAT), do: :float
  def from_enum(:TYPE_INT64), do: :int64
  def from_enum(:TYPE_UINT64), do: :uint64
  def from_enum(:TYPE_INT32), do: :int32
  def from_enum(:TYPE_FIXED64), do: :fixed64
  def from_enum(:TYPE_FIXED32), do: :fixed32
  def from_enum(:TYPE_BOOL), do: :bool
  def from_enum(:TYPE_STRING), do: :string
  def from_enum(:TYPE_GROUP), do: :group
  def from_enum(:TYPE_MESSAGE), do: :message
  def from_enum(:TYPE_BYTES), do: :bytes
  def from_enum(:TYPE_UINT32), do: :uint32
  def from_enum(:TYPE_ENUM), do: :enum
  def from_enum(:TYPE_SFIXED32), do: :sfixed32
  def from_enum(:TYPE_SFIXED64), do: :sfixed64
  def from_enum(:TYPE_SINT32), do: :sint32
  def from_enum(:TYPE_SINT64), do: :sint64

  def enum_to_spec(:TYPE_DOUBLE), do: "float"
  def enum_to_spec(:TYPE_FLOAT), do: "float"
  def enum_to_spec(:TYPE_INT64), do: "integer"
  def enum_to_spec(:TYPE_UINT64), do: "non_neg_integer"
  def enum_to_spec(:TYPE_INT32), do: "integer"
  def enum_to_spec(:TYPE_FIXED64), do: "non_neg_integer"
  def enum_to_spec(:TYPE_FIXED32), do: "non_neg_integer"
  def enum_to_spec(:TYPE_BOOL), do: "boolean"
  def enum_to_spec(:TYPE_STRING), do: "String.t"
  def enum_to_spec(:TYPE_BYTES), do: "binary"
  def enum_to_spec(:TYPE_UINT32), do: "non_neg_integer"
  def enum_to_spec(:TYPE_ENUM), do: "integer"
  def enum_to_spec(:TYPE_SFIXED32), do: "integer"
  def enum_to_spec(:TYPE_SFIXED64), do: "integer"
  def enum_to_spec(:TYPE_SINT32), do: "integer"
  def enum_to_spec(:TYPE_SINT64), do: "integer"
  def enum_to_spec(_), do: "any"
  def enum_to_spec(:TYPE_MESSAGE, type, true = _repeated), do: "#{type}.t"
  def enum_to_spec(:TYPE_MESSAGE, type, false = _repeated), do: "#{type}.t | nil"
end

defmodule Protobuf.TypeUtil do
  def from_enum(:TYPE_DOUBLE), do: :double
  def from_enum(:TYPE_FLOAT), do: :float
  def from_enum(:TYPE_INT64), do: :int64
  def from_enum(:TYPE_UINT64), do: :uint64
  def from_enum(:TYPE_INT32), do: :int32
  def from_enum(:TYPE_FIXED64), do: :fixed64
  def from_enum(:TYPE_FIXED32), do: :fixed32
  def from_enum(:TYPE_BOOL), do: :bool
  def from_enum(:TYPE_STRING), do: :string
  def from_enum(:TYPE_GROUP), do: :group
  def from_enum(:TYPE_MESSAGE), do: :message
  def from_enum(:TYPE_BYTES), do: :bytes
  def from_enum(:TYPE_UINT32), do: :uint32
  def from_enum(:TYPE_ENUM), do: :enum
  def from_enum(:TYPE_SFIXED32), do: :sfixed32
  def from_enum(:TYPE_SFIXED64), do: :sfixed64
  def from_enum(:TYPE_SINT32), do: :sint32
  def from_enum(:TYPE_SINT64), do: :sint64

  def enum_to_spec(:TYPE_DOUBLE), do: "float"
  def enum_to_spec(:TYPE_FLOAT), do: "float"
  def enum_to_spec(:TYPE_INT64), do: "integer"
  def enum_to_spec(:TYPE_UINT64), do: "non_neg_integer"
  def enum_to_spec(:TYPE_INT32), do: "integer"
  def enum_to_spec(:TYPE_FIXED64), do: "non_neg_integer"
  def enum_to_spec(:TYPE_FIXED32), do: "non_neg_integer"
  def enum_to_spec(:TYPE_BOOL), do: "boolean"
  def enum_to_spec(:TYPE_STRING), do: "String.t"
  def enum_to_spec(:TYPE_BYTES), do: "binary"
  def enum_to_spec(:TYPE_UINT32), do: "non_neg_integer"
  def enum_to_spec(:TYPE_ENUM), do: "integer"
  def enum_to_spec(:TYPE_SFIXED32), do: "integer"
  def enum_to_spec(:TYPE_SFIXED64), do: "integer"
  def enum_to_spec(:TYPE_SINT32), do: "integer"
  def enum_to_spec(:TYPE_SINT64), do: "integer"
  def enum_to_spec(:TYPE_MESSAGE, type, true = _repeated), do: "#{type}.t"
  def enum_to_spec(:TYPE_MESSAGE, type, false = _repeated), do: "#{type}.t | nil"
  def enum_to_spec(_), do: "any"
end


dot in mint docs

dot in mint docs

if Code.ensure_loaded?(Mint.HTTP) do
  defmodule Tesla.Adapter.Mint do
    @moduledoc """
    Adapter for [mint](https://github.com/elixir-mint/mint).

    Caution: The minimum supported Elixir version for mint is 1.5.0

    Remember to add `{:mint, "~> 1.0"}` and `{:castore, "~> 0.1"}` to dependencies.
    Also, you need to recompile tesla after adding `:mint` dependency:

    ```
    mix deps.clean tesla
    mix deps.compile tesla
    ```

    ## Example usage

    ```
    # set globally in config/config.exs
    config :tesla, :adapter, Tesla.Adapter.Mint
    # set per module
    defmodule MyClient do
      use Tesla
      adapter Tesla.Adapter.Mint
    end
    # set global custom cacertfile
    config :tesla, Tesla.Adapter.Mint, cacert: ["path_to_cacert"]
    ```

    ## Adapter specific options:

    - `:timeout` - Time in milliseconds, while process, will wait for mint messages. Defaults to `2_000`.
    - `:body_as` - What will be returned in `%Tesla.Env{}` body key. Possible values - `:plain`, `:stream`, `:chunks`. Defaults to `:plain`.
      - `:plain` - as binary.
      - `:stream` - as stream. If you don't want to close connection (because you want to reuse it later) pass `close_conn: false` in adapter opts.
      - `:chunks` - as chunks. You can get response body in chunks using `Tesla.Adapter.Mint.read_chunk/3` function.
      Processing of the chunks and checking body size must be done by yourself. Example of processing function is in `test/tesla/adapter/mint_test.exs` - `Tesla.Adapter.MintTest.read_body/4`. If you don't need connection later don't forget to close it with `Tesla.Adapter.Mint.close/1`.
    - `:max_body` - Max response body size in bytes. Works only with `body_as: :plain`, with other settings you need to check response body size by yourself.
    - `:conn` - Opened connection with mint. Is used for reusing mint connections.
    - `:original` - Original host with port, for which reused connection was open. Needed for `Tesla.Middleware.FollowRedirects`. Otherwise adapter will use connection for another open host.
    - `:close_conn` - Close connection or not after receiving full response body. Is used for reusing mint connections. Defaults to `true`.
    - `:proxy` - Proxy settings. E.g.: `{:http, "localhost", 8888, []}`, `{:http, "127.0.0.1", 8888, []}`
    """

    @behaviour Tesla.Adapter

    import Tesla.Adapter.Shared
    alias Tesla.Multipart
    alias Mint.HTTP

    @default timeout: 2_000, body_as: :plain, close_conn: true, mode: :active

    @impl Tesla.Adapter
    def call(env, opts) do
      opts = Tesla.Adapter.opts(@default, env, opts)

      with {:ok, status, headers, body} <- request(env, opts) do
        {:ok, %{env | status: status, headers: headers, body: body}}
      end
    end

    @doc """
    Reads chunk of the response body.
    Returns `{:fin, HTTP.t(), binary()}` if all body received, otherwise returns `{:nofin, HTTP.t(), binary()}`.
    """

    @spec read_chunk(HTTP.t(), reference(), keyword()) ::
            {:fin, HTTP.t(), binary()} | {:nofin, HTTP.t(), binary()}
    def read_chunk(conn, ref, opts) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts),
           {state, data} <- response_state(acc) do
        {:ok, conn} =
          if state == :fin and opts[:close_conn] do
            close(conn)
          else
            {:ok, conn}
          end

        {state, conn, data}
      end
    end

    @doc """
    Closes mint connection.
    """
    @spec close(HTTP.t()) :: {:ok, HTTP.t()}
    defdelegate close(conn), to: HTTP

    defp request(env, opts) do
      request(
        format_method(env.method),
        Tesla.build_url(env.url, env.query),
        env.headers,
        env.body,
        Enum.into(opts, %{})
      )
    end

    defp request(method, url, headers, %Stream{} = body, opts) do
      fun = stream_to_fun(body)
      request(method, url, headers, fun, opts)
    end

    defp request(method, url, headers, %Multipart{} = body, opts) do
      headers = headers ++ Multipart.headers(body)
      fun = stream_to_fun(Multipart.body(body))
      request(method, url, headers, fun, opts)
    end

    defp request(method, url, headers, body, opts),
      do: do_request(method, url, headers, body, opts)

    defp do_request(method, url, headers, body, opts) do
      with uri <- URI.parse(url),
           path <- prepare_path(uri.path, uri.query),
           opts <- check_original(uri, opts),
           {:ok, conn, opts} <- open_conn(uri, opts),
           {:ok, conn, ref} <- make_request(conn, method, path, headers, body) do
        format_response(conn, ref, opts)
      end
    end

    defp check_original(uri, %{original: original} = opts) do
      Map.put(opts, :original_matches, original == "#{uri.host}:#{uri.port}")
    end

    defp check_original(_uri, opts), do: opts

    defp open_conn(_uri, %{conn: conn, original_matches: true} = opts) do
      {:ok, conn, opts}
    end

    defp open_conn(uri, %{conn: conn, original_matches: false} = opts) do
      opts =
        opts
        |> Map.put_new(:old_conn, conn)
        |> Map.delete(:conn)

      open_conn(uri, opts)
    end

    defp open_conn(uri, opts) do
      opts =
        with "https" <- uri.scheme,
             global_cacertfile when not is_nil(global_cacertfile) <-
               Application.get_env(:tesla, Tesla.Adapter.Mint)[:cacert] do
          Map.update(opts, :transport_opts, [cacertfile: global_cacertfile], fn tr_opts ->
            Keyword.put_new(tr_opts, :cacertfile, global_cacertfile)
          end)
        else
          _ -> opts
        end

      with {:ok, conn} <-
             HTTP.connect(String.to_atom(uri.scheme), uri.host, uri.port, Enum.into(opts, [])) do
        # If there were redirects, and passed `closed_conn: false`, we need to close opened connections to these intermediate hosts.
        {:ok, conn, Map.put(opts, :close_conn, true)}
      end
    end

    defp make_request(conn, method, path, headers, body) when is_function(body) do
      with {:ok, conn, ref} <-
             HTTP.request(
               conn,
               method,
               path,
               headers,
               :stream
             ),
           {:ok, conn} <- stream_request(conn, ref, body) do
        {:ok, conn, ref}
      end
    end

    defp make_request(conn, method, path, headers, body),
      do: HTTP.request(conn, method, path, headers, body)

    defp stream_request(conn, ref, fun) do
      case next_chunk(fun) do
        {:ok, item, fun} when is_list(item) ->
          chunk = List.to_string(item)
          {:ok, conn} = HTTP.stream_request_body(conn, ref, chunk)
          stream_request(conn, ref, fun)

        {:ok, item, fun} ->
          {:ok, conn} = HTTP.stream_request_body(conn, ref, item)
          stream_request(conn, ref, fun)

        :eof ->
          HTTP.stream_request_body(conn, ref, :eof)
      end
    end

    defp format_response(conn, ref, %{body_as: :plain} = opts) do
      with {:ok, response} <- receive_responses(conn, ref, opts) do
        {:ok, response[:status], response[:headers], response[:data]}
      end
    end

    defp format_response(conn, ref, %{body_as: :chunks} = opts) do
      with {:ok, conn, %{status: status, headers: headers} = acc} <-
             receive_headers_and_status(conn, ref, opts),
           {state, data} <-
             response_state(acc) do
        {:ok, conn} =
          if state == :fin and opts[:close_conn] do
            close(conn)
          else
            {:ok, conn}
          end

        {:ok, status, headers, %{conn: conn, ref: ref, opts: opts, body: {state, data}}}
      end
    end

    defp format_response(conn, ref, %{body_as: :stream} = opts) do
      # there can be some data already
      with {:ok, conn, %{status: status, headers: headers} = acc} <-
             receive_headers_and_status(conn, ref, opts) do
        body_as_stream =
          Stream.resource(
            fn -> %{conn: conn, data: acc[:data], done: acc[:done]} end,
            fn
              %{conn: conn, data: data, done: true} ->
                {[data], %{conn: conn, is_fin: true}}

              %{conn: conn, data: data} when is_binary(data) ->
                {[data], %{conn: conn}}

              %{conn: conn, is_fin: true} ->
                {:halt, %{conn: conn}}

              %{conn: conn} ->
                case receive_packet(conn, ref, opts) do
                  {:ok, conn, %{done: true, data: data}} ->
                    {[data], %{conn: conn, is_fin: true}}

                  {:ok, conn, %{done: true}} ->
                    {[], %{conn: conn, is_fin: true}}

                  {:ok, conn, %{data: data}} ->
                    {[data], %{conn: conn}}

                  {:ok, conn, _} ->
                    {[], %{conn: conn}}
                end
            end,
            fn %{conn: conn} -> if opts[:close_conn], do: {:ok, _conn} = close(conn) end
          )

        {:ok, status, headers, body_as_stream}
      end
    end

    defp receive_responses(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts, acc),
           :ok <- check_data_size(acc, conn, opts) do
        if acc[:done] do
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:ok, acc}
        else
          receive_responses(conn, ref, opts, acc)
        end
      end
    end

    defp check_data_size(%{data: data}, conn, %{max_body: max_body} = opts)
         when is_binary(data) do
      if max_body - byte_size(data) >= 0 do
        :ok
      else
        if opts[:close_conn], do: {:ok, _conn} = close(conn)
        {:error, :body_too_large}
      end
    end

    defp check_data_size(_, _, _), do: :ok

    defp receive_headers_and_status(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts, acc) do
        case acc do
          %{status: _status, headers: _headers} -> {:ok, conn, acc}
          # if we don't have status or headers we try to get them in next packet
          _ -> receive_headers_and_status(conn, ref, opts, acc)
        end
      end
    end

    defp response_state(%{done: true, data: data}), do: {:fin, data}
    defp response_state(%{data: data}), do: {:nofin, data}
    defp response_state(%{done: true}), do: {:fin, ""}
    defp response_state(_), do: {:nofin, ""}

    defp receive_packet(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, responses} <- receive_message(conn, opts),
           acc <- reduce_responses(responses, ref, acc) do
        {:ok, conn, acc}
      else
        {:error, error} ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, error}

        {:error, _conn, error, _res} ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, "Encounter Mint error #{inspect(error)}"}

        :unknown ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, :unknown}
      end
    end

    defp receive_message(conn, %{mode: :active} = opts) do
      receive do
        message ->
          HTTP.stream(conn, message)
      after
        opts[:timeout] -> {:error, :timeout}
      end
    end

    defp receive_message(conn, %{mode: :passive} = opts),
      do: HTTP.recv(conn, 0, opts[:timeout])

    defp reduce_responses(responses, ref, acc) do
      Enum.reduce(responses, acc, fn
        {:status, ^ref, code}, acc ->
          Map.put(acc, :status, code)

        {:headers, ^ref, headers}, acc ->
          Map.update(acc, :headers, headers, &(&1 ++ headers))

        {:data, ^ref, data}, acc ->
          Map.update(acc, :data, data, &(&1 <> data))

        {:done, ^ref}, acc ->
          Map.put(acc, :done, true)
      end)
    end
  end
end

if Code.ensure_loaded?(Mint.HTTP) do
  defmodule Tesla.Adapter.Mint do
    @moduledoc """
    Adapter for [mint](https://github.com/elixir-mint/mint).

    Caution: The minimum supported Elixir version for mint is 1.5.0

    Remember to add `{:mint, "~> 1.0"}` and `{:castore, "~> 0.1"}` to dependencies
    Also, you need to recompile tesla after adding `:mint` dependency:

    ```
    mix deps.clean tesla
    mix deps.compile tesla
    ```

    ## Example usage

    ```
    # set globally in config/config.exs
    config :tesla, :adapter, Tesla.Adapter.Mint
    # set per module
    defmodule MyClient do
      use Tesla
      adapter Tesla.Adapter.Mint
    end
    # set global custom cacertfile
    config :tesla, Tesla.Adapter.Mint, cacert: ["path_to_cacert"]
    ```

    ## Adapter specific options:

    - `:timeout` - Time in milliseconds, while process, will wait for mint messages. Defaults to `2_000`.
    - `:body_as` - What will be returned in `%Tesla.Env{}` body key. Possible values - `:plain`, `:stream`, `:chunks`. Defaults to `:plain`.
      - `:plain` - as binary.
      - `:stream` - as stream. If you don't want to close connection (because you want to reuse it later) pass `close_conn: false` in adapter opts.
      - `:chunks` - as chunks. You can get response body in chunks using `Tesla.Adapter.Mint.read_chunk/3` function.
      Processing of the chunks and checking body size must be done by yourself. Example of processing function is in `test/tesla/adapter/mint_test.exs` - `Tesla.Adapter.MintTest.read_body/4`. If you don't need connection later don't forget to close it with `Tesla.Adapter.Mint.close/1`.
    - `:max_body` - Max response body size in bytes. Works only with `body_as: :plain`, with other settings you need to check response body size by yourself.
    - `:conn` - Opened connection with mint. Is used for reusing mint connections.
    - `:original` - Original host with port, for which reused connection was open. Needed for `Tesla.Middleware.FollowRedirects`. Otherwise adapter will use connection for another open host.
    - `:close_conn` - Close connection or not after receiving full response body. Is used for reusing mint connections. Defaults to `true`.
    - `:proxy` - Proxy settings. E.g.: `{:http, "localhost", 8888, []}`, `{:http, "127.0.0.1", 8888, []}`
    """

    @behaviour Tesla.Adapter

    import Tesla.Adapter.Shared
    alias Tesla.Multipart
    alias Mint.HTTP

    @default timeout: 2_000, body_as: :plain, close_conn: true, mode: :active

    @impl Tesla.Adapter
    def call(env, opts) do
      opts = Tesla.Adapter.opts(@default, env, opts)

      with {:ok, status, headers, body} <- request(env, opts) do
        {:ok, %{env | status: status, headers: headers, body: body}}
      end
    end

    @doc """
    Reads chunk of the response body.
    Returns `{:fin, HTTP.t(), binary()}` if all body received, otherwise returns `{:nofin, HTTP.t(), binary()}`.
    """

    @spec read_chunk(HTTP.t(), reference(), keyword()) ::
            {:fin, HTTP.t(), binary()} | {:nofin, HTTP.t(), binary()}
    def read_chunk(conn, ref, opts) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts),
           {state, data} <- response_state(acc) do
        {:ok, conn} =
          if state == :fin and opts[:close_conn] do
            close(conn)
          else
            {:ok, conn}
          end

        {state, conn, data}
      end
    end

    @doc """
    Closes mint connection.
    """
    @spec close(HTTP.t()) :: {:ok, HTTP.t()}
    defdelegate close(conn), to: HTTP

    defp request(env, opts) do
      request(
        format_method(env.method),
        Tesla.build_url(env.url, env.query),
        env.headers,
        env.body,
        Enum.into(opts, %{})
      )
    end

    defp request(method, url, headers, %Stream{} = body, opts) do
      fun = stream_to_fun(body)
      request(method, url, headers, fun, opts)
    end

    defp request(method, url, headers, %Multipart{} = body, opts) do
      headers = headers ++ Multipart.headers(body)
      fun = stream_to_fun(Multipart.body(body))
      request(method, url, headers, fun, opts)
    end

    defp request(method, url, headers, body, opts),
      do: do_request(method, url, headers, body, opts)

    defp do_request(method, url, headers, body, opts) do
      with uri <- URI.parse(url),
           path <- prepare_path(uri.path, uri.query),
           opts <- check_original(uri, opts),
           {:ok, conn, opts} <- open_conn(uri, opts),
           {:ok, conn, ref} <- make_request(conn, method, path, headers, body) do
        format_response(conn, ref, opts)
      end
    end

    defp check_original(uri, %{original: original} = opts) do
      Map.put(opts, :original_matches, original == "#{uri.host}:#{uri.port}")
    end

    defp check_original(_uri, opts), do: opts

    defp open_conn(_uri, %{conn: conn, original_matches: true} = opts) do
      {:ok, conn, opts}
    end

    defp open_conn(uri, %{conn: conn, original_matches: false} = opts) do
      opts =
        opts
        |> Map.put_new(:old_conn, conn)
        |> Map.delete(:conn)

      open_conn(uri, opts)
    end

    defp open_conn(uri, opts) do
      opts =
        with "https" <- uri.scheme,
             global_cacertfile when not is_nil(global_cacertfile) <-
               Application.get_env(:tesla, Tesla.Adapter.Mint)[:cacert] do
          Map.update(opts, :transport_opts, [cacertfile: global_cacertfile], fn tr_opts ->
            Keyword.put_new(tr_opts, :cacertfile, global_cacertfile)
          end)
        else
          _ -> opts
        end

      with {:ok, conn} <-
             HTTP.connect(String.to_atom(uri.scheme), uri.host, uri.port, Enum.into(opts, [])) do
        # If there were redirects, and passed `closed_conn: false`, we need to close opened connections to these intermediate hosts.
        {:ok, conn, Map.put(opts, :close_conn, true)}
      end
    end

    defp make_request(conn, method, path, headers, body) when is_function(body) do
      with {:ok, conn, ref} <-
             HTTP.request(
               conn,
               method,
               path,
               headers,
               :stream
             ),
           {:ok, conn} <- stream_request(conn, ref, body) do
        {:ok, conn, ref}
      end
    end

    defp make_request(conn, method, path, headers, body),
      do: HTTP.request(conn, method, path, headers, body)

    defp stream_request(conn, ref, fun) do
      case next_chunk(fun) do
        {:ok, item, fun} when is_list(item) ->
          chunk = List.to_string(item)
          {:ok, conn} = HTTP.stream_request_body(conn, ref, chunk)
          stream_request(conn, ref, fun)

        {:ok, item, fun} ->
          {:ok, conn} = HTTP.stream_request_body(conn, ref, item)
          stream_request(conn, ref, fun)

        :eof ->
          HTTP.stream_request_body(conn, ref, :eof)
      end
    end

    defp format_response(conn, ref, %{body_as: :plain} = opts) do
      with {:ok, response} <- receive_responses(conn, ref, opts) do
        {:ok, response[:status], response[:headers], response[:data]}
      end
    end

    defp format_response(conn, ref, %{body_as: :chunks} = opts) do
      with {:ok, conn, %{status: status, headers: headers} = acc} <-
             receive_headers_and_status(conn, ref, opts),
           {state, data} <-
             response_state(acc) do
        {:ok, conn} =
          if state == :fin and opts[:close_conn] do
            close(conn)
          else
            {:ok, conn}
          end

        {:ok, status, headers, %{conn: conn, ref: ref, opts: opts, body: {state, data}}}
      end
    end

    defp format_response(conn, ref, %{body_as: :stream} = opts) do
      # there can be some data already
      with {:ok, conn, %{status: status, headers: headers} = acc} <-
             receive_headers_and_status(conn, ref, opts) do
        body_as_stream =
          Stream.resource(
            fn -> %{conn: conn, data: acc[:data], done: acc[:done]} end,
            fn
              %{conn: conn, data: data, done: true} ->
                {[data], %{conn: conn, is_fin: true}}

              %{conn: conn, data: data} when is_binary(data) ->
                {[data], %{conn: conn}}

              %{conn: conn, is_fin: true} ->
                {:halt, %{conn: conn}}

              %{conn: conn} ->
                case receive_packet(conn, ref, opts) do
                  {:ok, conn, %{done: true, data: data}} ->
                    {[data], %{conn: conn, is_fin: true}}

                  {:ok, conn, %{done: true}} ->
                    {[], %{conn: conn, is_fin: true}}

                  {:ok, conn, %{data: data}} ->
                    {[data], %{conn: conn}}

                  {:ok, conn, _} ->
                    {[], %{conn: conn}}
                end
            end,
            fn %{conn: conn} -> if opts[:close_conn], do: {:ok, _conn} = close(conn) end
          )

        {:ok, status, headers, body_as_stream}
      end
    end

    defp receive_responses(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts, acc),
           :ok <- check_data_size(acc, conn, opts) do
        if acc[:done] do
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:ok, acc}
        else
          receive_responses(conn, ref, opts, acc)
        end
      end
    end

    defp check_data_size(%{data: data}, conn, %{max_body: max_body} = opts)
         when is_binary(data) do
      if max_body - byte_size(data) >= 0 do
        :ok
      else
        if opts[:close_conn], do: {:ok, _conn} = close(conn)
        {:error, :body_too_large}
      end
    end

    defp check_data_size(_, _, _), do: :ok

    defp receive_headers_and_status(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, acc} <- receive_packet(conn, ref, opts, acc) do
        case acc do
          %{status: _status, headers: _headers} -> {:ok, conn, acc}
          # if we don't have status or headers we try to get them in next packet
          _ -> receive_headers_and_status(conn, ref, opts, acc)
        end
      end
    end

    defp response_state(%{done: true, data: data}), do: {:fin, data}
    defp response_state(%{data: data}), do: {:nofin, data}
    defp response_state(%{done: true}), do: {:fin, ""}
    defp response_state(_), do: {:nofin, ""}

    defp receive_packet(conn, ref, opts, acc \\ %{}) do
      with {:ok, conn, responses} <- receive_message(conn, opts),
           acc <- reduce_responses(responses, ref, acc) do
        {:ok, conn, acc}
      else
        {:error, error} ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, error}

        {:error, _conn, error, _res} ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, "Encounter Mint error #{inspect(error)}"}

        :unknown ->
          if opts[:close_conn], do: {:ok, _conn} = close(conn)
          {:error, :unknown}
      end
    end

    defp receive_message(conn, %{mode: :active} = opts) do
      receive do
        message ->
          HTTP.stream(conn, message)
      after
        opts[:timeout] -> {:error, :timeout}
      end
    end

    defp receive_message(conn, %{mode: :passive} = opts),
      do: HTTP.recv(conn, 0, opts[:timeout])

    defp reduce_responses(responses, ref, acc) do
      Enum.reduce(responses, acc, fn
        {:status, ^ref, code}, acc ->
          Map.put(acc, :status, code)

        {:headers, ^ref, headers}, acc ->
          Map.update(acc, :headers, headers, &(&1 ++ headers))

        {:data, ^ref, data}, acc ->
          Map.update(acc, :data, data, &(&1 <> data))

        {:done, ^ref}, acc ->
          Map.put(acc, :done, true)
      end)
    end
  end
end


Implement parsing

Implement parsing

This is as recommended by the book, yet my feeling is that I would
probably structure it a bit differently.

defmodule Issues.CLI do
  @default_count 4
  @moduledoc """
    Handle the command line parsing and the dispatch to
    the various functions that end up generating a table of the last
    _n_ issues in a github project
  """

  def run(argv) do
    parse_args(argv)
  end

  @doc """
    `argv` can be -h or --help, which returns :help.

    Otherwise it is a github user name, project name, and (optionally)
    the number of entries to format.

    Return a tuple of `{ user, project, count }` or `:help` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [help: :boolean],
                                     aliases: [h: :help])
    case parse do
      {[help: true], _, _} -> :help
      {_, [user, project, count], _} -> {user, project, count}
      {_, [user, project], _} -> {user, project, @default_count}
      _ -> :help
    end
  end
end

defmodule Issues.CLI do
  @default_count 4
  @moduledoc """
    Handle the command line parsing and the dispatch to
    the various functions that end up generating a table of the last
    _n_ issues in a github project
  """

  def run(argv) do
    parse_args(argv)
  end

  @doc """
    `argv` can be -h or --help, which returns :help.

    Otherwise it is a github user name, project name, and (optionally)
    the number of entries to format.

    Return a tuple of `{ user, project, count }` or `:help` if help was given.
  """
  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [help: :boolean],
                                     aliases: [h: :help])
  end
end


send hook to vol_event_submission on confirm

defmodule Admin.Webhooks do
  require Logger
  import ShortMaps

  @instance Application.get_env(:admin, :instance, "jd")
  @cosmic_config_slug Application.get_env(:admin, :cosmic_info_slug)

  def on(hook, body = %{event: event}) do
    processed = process_event(event)

    hook
    |> exec(Map.put(body, :event, processed))
  end

  def exec("confirmed", contents = %{event: event, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_publish" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of confirmed")
      IO.inspect(HTTPotion.post(hook, bodify(%{event: event, team_member: team_member})))
    else
      %{"metadata" => %{"vol_event_submission" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of confirmed vol event")
      IO.inspect(HTTPotion.post(hook, bodify(%{event: event, team_member: team_member})))

      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect(
        Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Published"), info)
      )
    end
  end

  def exec("rejected", contents = %{event: event, reason: reason, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_rejected" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of rejected")

      IO.inspect(
        HTTPotion.post(hook, bodify(%{event: event, reason: reason, team_member: team_member}))
      )
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect(
        Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Rejected"), info)
      )
    end
  end

  def exec("cancelled", contents = %{event: event, team_member: team_member, reason: reason}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_cancelled" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of cancelled")

      IO.inspect(
        HTTPotion.post(hook, bodify(%{event: event, team_member: team_member, reason: reason}))
      )
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect(
        Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Cancelled"), info)
      )
    end
  end

  def exec("tentative", contents = %{event: event, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_unpublished" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of tentative")
      IO.inspect(HTTPotion.post(hook, bodify(%{event: event, team_member: team_member})))
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect(
        Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Unpublished"), info)
      )
    end
  end

  def exec("edit", %{event: event, edits: edits}) do
    %{"metadata" => %{"event_edited" => hook}} = Cosmic.get(@cosmic_config_slug)
    IO.puts("Posting webhook to #{hook} because of edit")
    IO.inspect(HTTPotion.post(hook, bodify(%{event: event, edits: edits})))
  end

  def exec("message_host", contents = ~m(event host message)a) do
    if @instance != "jd" do
      %{"metadata" => %{"message_host" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of message-host")
      IO.inspect(HTTPotion.post(hook, bodify(~m(event host message))))
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect(
        Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Message Host"), info)
      )
    end
  end

  def exec(hook_type = "message_attendees" <> _rest, ~m(event attendee_emails message)a) do
    hook = Cosmic.get(@cosmic_config_slug) |> get_in(["metadata", hook_type])
    IO.puts("Posting webhook to #{hook} because of #{hook_type}")
    IO.inspect(HTTPotion.post(hook, bodify(~m(event attendee_emails message))))
  end

  def exec(other, %{event: event, team_member: _team_member}) do
    Logger.info("Untracked status change: #{other}, for event: #{inspect(event)}")
  end

  defp bodify(body), do: [body: Poison.encode!(IO.inspect(body))]

  defp process_event(event) do
    event
    |> Map.put(:date_line, get_date_line(event))
  end

  def get_date_line(event) do
    humanize_date(event.start_date) <>
      "from " <> humanize_time(event.start_date) <> " - " <> humanize_time(event.end_date)
  end

  defp humanize_date(dt) do
    %DateTime{month: month, day: day} = parse(dt)

    month =
      [
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December"
      ]
      |> Enum.at(month - 1)

    "#{month} #{day} "
  end

  defp humanize_time(dt) do
    %DateTime{hour: hour, minute: minute} = parse(dt)

    {hour, am_pm} = if hour >= 12, do: {hour - 12, "PM"}, else: {hour, "AM"}
    hour = if hour == 0, do: 12, else: hour
    minute = if minute == 0, do: "", else: ":#{minute}"

    "#{hour}#{minute} " <> am_pm
  end

  def parse(nil) do
    DateTime.utc_now()
  end

  def zero_pad(int) do
    str = "#{int}"
    if String.length(str), do: str, else: "0#{str}"
  end

  def parse(dt = %DateTime{}) do
    dt
  end

  def parse(dt) do
    iso = if String.ends_with?(dt, "Z"), do: dt, else: dt <> "Z"
    {:ok, result, _} = DateTime.from_iso8601(iso)
    result
  end

  def flatten(%{} = map) do
    map
    |> Map.drop(~w(name))
    |> Map.to_list()
    |> to_flat_map(%{})
  end

  def flatten(%{} = map) when map == %{}, do: %{}

  defp to_flat_map([{_k, %{} = v} | t], acc), do: to_flat_map(Map.to_list(v), to_flat_map(t, acc))
  defp to_flat_map([{k, v} | t], acc), do: to_flat_map(t, Map.put_new(acc, k, v))
  defp to_flat_map([], acc), do: acc
end

defmodule Admin.Webhooks do
  require Logger
  import ShortMaps

  @instance Application.get_env(:admin, :instance, "jd")
  @cosmic_config_slug Application.get_env(:admin, :cosmic_info_slug)

  def on(hook, body = %{event: event}) do
    processed = process_event(event)

    hook
    |> exec(Map.put(body, :event, processed))
  end

  def exec("confirmed", contents = %{event: event, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_publish" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of confirmed")
      IO.inspect(HTTPotion.post(hook, bodify(%{event: event, team_member: team_member})))
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Published"), info)
    end
  end

  def exec("rejected", contents = %{event: event, reason: reason, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_rejected" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of rejected")

      IO.inspect(
        HTTPotion.post(hook, bodify(%{event: event, reason: reason, team_member: team_member}))
      )
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Rejected"), info)
    end
  end

  def exec("cancelled", contents = %{event: event, team_member: team_member, reason: reason}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_cancelled" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of cancelled")

      IO.inspect(
        HTTPotion.post(hook, bodify(%{event: event, team_member: team_member, reason: reason}))
      )
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Cancelled"), info)
    end
  end

  def exec("tentative", contents = %{event: event, team_member: team_member}) do
    if @instance != "jd" do
      %{"metadata" => %{"event_unpublished" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of tentative")
      IO.inspect(HTTPotion.post(hook, bodify(%{event: event, team_member: team_member})))
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Unpublished"), info)
    end
  end

  def exec("edit", %{event: event, edits: edits}) do
    %{"metadata" => %{"event_edited" => hook}} = Cosmic.get(@cosmic_config_slug)
    IO.puts("Posting webhook to #{hook} because of edit")
    IO.inspect(HTTPotion.post(hook, bodify(%{event: event, edits: edits})))
  end

  def exec("message_host", contents = ~m(event host message)a) do
    if @instance != "jd" do
      %{"metadata" => %{"message_host" => hook}} = Cosmic.get(@cosmic_config_slug)
      IO.puts("Posting webhook to #{hook} because of message-host")
      IO.inspect(HTTPotion.post(hook, bodify(~m(event host message))))
    else
      email = event.contact.email_address

      info =
        flatten(contents)
        |> Enum.map(fn {key, val} ->
          {"action_#{key}", val}
        end)
        |> Enum.into(~m(email))

      IO.inspect Ak.Signup.process_signup(&String.contains?(&1["title"], "ESM: Event Message Host"), info)
    end
  end

  def exec(hook_type = "message_attendees" <> _rest, ~m(event attendee_emails message)a) do
    hook = Cosmic.get(@cosmic_config_slug) |> get_in(["metadata", hook_type])
    IO.puts("Posting webhook to #{hook} because of #{hook_type}")
    IO.inspect(HTTPotion.post(hook, bodify(~m(event attendee_emails message))))
  end

  def exec(other, %{event: event, team_member: _team_member}) do
    Logger.info("Untracked status change: #{other}, for event: #{inspect(event)}")
  end

  defp bodify(body), do: [body: Poison.encode!(IO.inspect(body))]

  defp process_event(event) do
    event
    |> Map.put(:date_line, get_date_line(event))
  end

  def get_date_line(event) do
    humanize_date(event.start_date) <>
      "from " <> humanize_time(event.start_date) <> " - " <> humanize_time(event.end_date)
  end

  defp humanize_date(dt) do
    %DateTime{month: month, day: day} = parse(dt)

    month =
      [
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December"
      ]
      |> Enum.at(month - 1)

    "#{month} #{day} "
  end

  defp humanize_time(dt) do
    %DateTime{hour: hour, minute: minute} = parse(dt)

    {hour, am_pm} = if hour >= 12, do: {hour - 12, "PM"}, else: {hour, "AM"}
    hour = if hour == 0, do: 12, else: hour
    minute = if minute == 0, do: "", else: ":#{minute}"

    "#{hour}#{minute} " <> am_pm
  end

  def parse(nil) do
    DateTime.utc_now()
  end

  def zero_pad(int) do
    str = "#{int}"
    if String.length(str), do: str, else: "0#{str}"
  end

  def parse(dt = %DateTime{}) do
    dt
  end

  def parse(dt) do
    iso = if String.ends_with?(dt, "Z"), do: dt, else: dt <> "Z"
    {:ok, result, _} = DateTime.from_iso8601(iso)
    result
  end

  def flatten(%{} = map) do
    map
    |> Map.drop(~w(name))
    |> Map.to_list()
    |> to_flat_map(%{})
  end

  def flatten(%{} = map) when map == %{}, do: %{}

  defp to_flat_map([{_k, %{} = v} | t], acc), do: to_flat_map(Map.to_list(v), to_flat_map(t, acc))
  defp to_flat_map([{k, v} | t], acc), do: to_flat_map(t, Map.put_new(acc, k, v))
  defp to_flat_map([], acc), do: acc
end


style

style

defmodule GenServerring do
  use GenServer
  require Crdtex.Set

  defstruct(
    node_set: Crdtex.Set.new,
    up_set: MapSet.new,
    forced_down: Crdtex.Set.new,
    payload: nil,
    counter: 0,
    callback: nil)

  defmacro __using__(_), do: []

  def start_link({name, callback}) do
    {:ok, payload} = callback.init([])
    GenServer.start_link(__MODULE__, {payload, callback}, [{:name, name}])
  end

  def init({payload, callback}) do
    :erlang.send_after(1_000, self(), :send_gossip)
    case File.read(ring_path) do
      {:ok, bin} ->
        set = :erlang.binary_to_term(bin)
        monitor(get_set(set))
        # should we call callback.handle_ring_change in such a situation ?
        {:ok, gen_ring(set, MapSet.new(get_set(set)), payload, 0, callback)}
      _ ->
        set = Crdtex.Set.new
        {:ok, set} = add(set, {node(), 1}, node())
        {:ok, gen_ring(set, MapSet.new([node()]), payload, 1, callback)}
    end
  end

  # payload management, mimicking a GenServer
  def call(server, action), do: GenServer.call(server, action)

  def cast(server, action), do: GenServer.cast(server, action)

  def reply(client, term), do: GenServer.reply(client, term)

  # cluster_management
  # we have to stop all nodes
  def stop({name, n} = server, reason \\ :normal, timeout \\ :infinity) do
    {:ok, ring} = get_ring(server)
    others = ring.up_set |> MapSet.delete(n)
    Enum.each(others, fn(n) -> GenServer.stop({name, n}, reason, timeout) end)
    Genserver.stop(server, reason, timeout)
  end

  def add_node(server, node) when is_binary(node),
    do: add_node(server, :"#{node}")
  def add_node(server, node) when is_atom(node),
    do: GenServer.cast(server, {:add_node, node})

  def del_node(server, node) when is_binary(node),
    do: del_node(server, :"#{node}")
  def del_node(server, node) when is_atom(node),
    do: GenServer.cast(server, {:del_node, node})

  # forbid the use of the given node (no gossip sent to it, gossip from it
  # ignored) but it might still be up, it will continue to be monitored as long
  # as it is up.
  def force_down(server, n), do:  GenServer.call(server, {:forced_down, n})

  # remove a node of the forced_down state
  def unforce_down(server, n), do:  GenServer.call(server, {:unforced_down, n})

  def all(server), do: GenServer.call(server, :get_all)
  def up(server), do: GenServer.call(server, :get_up)

  # classic GenServer callbacks
  def handle_call(:get_all, _, ring), do: {:reply, get_set(ring.node_set), ring}
  def handle_call(:get_up, _, ring),
    do: {:reply, MapSet.to_list(up_nodes(ring)), ring}
  def handle_call(:get_ring, _, ring), do: {:reply, {:ok, ring}, ring}
  def handle_call({:forced_down, n}, _, ring) do
    counter = ring.counter + 1
    {:ok, forced_down} = add(ring.forced_down, {node(), counter}, n)
    {:reply, get_set(forced_down),
      %{ring | forced_down: forced_down, counter: counter}}
  end
  def handle_call({:unforced_down, n}, _, ring) do
    counter = ring.counter + 1
    {:ok, forced_down} = delete(ring.forced_down, {node(), counter}, n)
    up_set = ring.up_set
    up_set =
      case MapSet.member?(up_set, n) do
        true -> up_set
        false ->
          Node.monitor(n, true)
          MapSet.put(up_set, n)
      end
    {:reply, get_set(forced_down),
      %{ring | forced_down: forced_down, up_set: up_set, counter: counter}}
  end
  def handle_call(other, from, ring) do
    payload = ring.payload
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_call(other, from, payload) do
      {:reply, reply, new} -> {:reply, reply, up_load.(new)}
      {:reply, reply, new, timeout} -> {:reply, reply, up_load.(new), timeout}
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, reply, new} -> {:stop, reason, reply, up_load.(new)}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  def handle_cast({:reconcile, gossip}, ring) do
    from_node = gossip.from_node
    up_set_reconcile(from_node, ring.up_set)
    ring_reconcile(from_node, gossip, ring)
  end
  def handle_cast({:add_node, n}, ring) do
    case contain?(ring.node_set, n) do
      true ->
        {:noreply, ring}
      false ->
        new_up_set = MapSet.put(ring.up_set, n)
        {:ok, new_node_set} = add(ring.node_set, {node(), ring.counter + 1}, n)
        {ring, _} =
          update_ring(%GenServerring{ring | up_set: new_up_set},
           %{node_set: new_node_set, payload: ring.payload, from_node: [],
             forced_down: ring.forced_down})
        {:noreply, ring}
    end
  end
  def handle_cast({:del_node, n}, ring) do
    case contain?(ring.node_set, n) do
      false ->
        {:noreply, ring}
      true ->
        new_up_set = MapSet.delete(ring.up_set, n)
        {:ok, new_node_set} =
          delete(ring.node_set, {node(), ring.counter + 1}, n)
        {ring, _} =
         update_ring(%GenServerring{ring | up_set: new_up_set},
           %{node_set: new_node_set, payload: ring.payload, from_node: [],
             forced_down: ring.forced_down})
        {:noreply, ring}
    end
  end
  def handle_cast(other, ring) do
    payload = ring.payload
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_cast(other, payload) do
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  def handle_info(:send_gossip, %GenServerring{node_set: node_set} = ring) do
    :erlang.send_after(1_000, self(), :send_gossip)
    if not contain?(node_set, node()) do
      :erlang.send_after(5_000, self(), :halt_node)
    end
    case up_nodes(ring) |> MapSet.delete(node()) |> MapSet.to_list do
      [] -> {:noreply, ring}
      active_nodes ->
        {:registered_name, name} = Process.info(self(), :registered_name)
        random_node =
          Enum.at(active_nodes, :random.uniform(length(active_nodes)) - 1)
        GenServer.cast(
          {name, random_node},
          {:reconcile,
            %{node_set: node_set,
              forced_down: ring.forced_down,
              payload: ring.payload,
              from_node: [node()]}})
      {:noreply, ring}
    end
  end
  def handle_info({:nodedown, n}, %GenServerring{up_set: up_set} = ring) do
    new_up_set =
      case MapSet.member?(up_set, n) do
        true ->
          set = MapSet.delete(up_set, n)
          {:registered_name, name} = Process.info(self(), :registered_name)
          ring.callback.handle_ring_change({MapSet.to_list(set), name,
            :nodedown})
          set
        false -> up_set
      end
    {:noreply, %{ring | up_set: new_up_set}}
  end
  def handle_info(:halt_node, s) do
    File.rm(ring_path)
    :init.stop()
    {:noreply, s}
  end
  def handle_info(other, ring) do
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_info(other, ring.payload) do
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  # small utilities functions
  defp value(payload), do: Crdtex.value(payload)

  defp get_set(set), do: Crdtex.value(set)

  defp contain?(set, e), do: Crdtex.value(set, {:contains, e})

  defp add(set, actor, e), do: Crdtex.update(set, actor, {:add, e})

  defp delete(set, actor, e), do: Crdtex.update(set, actor, {:remove, e})

  defp merge(nil, crdt), do: crdt
  defp merge(crdt, crdt), do: crdt
  defp merge(crdt1, crdt2), do: Crdtex.merge(crdt1, crdt2)

  defp get_ring(server), do: GenServer.call(server, :get_ring)

  defp up_nodes(ring),
    do: MapSet.difference(ring.up_set, MapSet.new(get_set(ring.forced_down)))

  defp monitor(list) do
    list = List.delete(list, node())
    Enum.each(list, fn(n) -> Node.monitor(n, :true) end)
  end

  defp gen_ring(set, up, payload, counter, callback),
    do: gen_ring(set, up, payload, counter, callback, Crdtex.Set.new)
  defp gen_ring(set, up, payload, counter, callback, forced_down) do
    %GenServerring{node_set: set, up_set: up, forced_down: forced_down,
      payload: payload, counter: counter, callback: callback}
  end

  defp update_payload(ring, payload) do
    {ring, _} =
      update_ring(ring,
        %{node_set: ring.node_set, payload: payload, from_node: [],
          forced_down: ring.forced_down})
    ring
  end

  defp update_ring(ring, changes) do
    merged_node_set = merge(ring.node_set, changes.node_set)
    merged_payload = merge(ring.payload, changes.payload)
    merged_forced_down = merge(ring.forced_down, changes.forced_down)
    updated_counter = update_counter(merged_node_set, merged_payload, ring)

    up_set =
      notify_up_set(get_set(ring.node_set), get_set(merged_node_set),
        ring.up_set, changes.from_node, ring.callback)
    notify_node_set(ring.node_set, merged_node_set, changes.node_set)

    old_payload = ring.payload
    ring =
      gen_ring(merged_node_set, up_set, merged_payload, updated_counter,
        ring.callback, merged_forced_down)
    {ring, notify_payload(value(old_payload), value(changes.payload), ring)}
  end

  defp update_counter(merged_node_set, merged_payload, old_ring) do
    old_nodes = old_ring.node_set
    old_payload = old_ring.payload
    case {merged_node_set, merged_payload} do
      {^old_nodes, ^old_payload} -> old_ring.counter
      _ -> old_ring.counter + 1
    end
  end

  defp notify_up_set(set, set, old_up, [], _), do: old_up
  defp notify_up_set(set, set, old_up, [n], callback) do
    case MapSet.member?(old_up, n) do
      true -> old_up
      false -> notify_up_set(set, set, MapSet.put(old_up, n), callback)
    end
  end
  defp notify_up_set(old, merged, up, [], callback),
    do: notify_up_set(old, merged, up, callback)
  defp notify_up_set(old, merged, up, [n], callback),
    do: notify_up_set(old, merged, MapSet.put(up, n), callback)

  defp notify_up_set(old_set, merged_set, old_up, callback) do
    new_up = MapSet.difference(MapSet.new(merged_set), MapSet.new(old_set))
    Enum.each(new_up, fn(n) -> Node.monitor(n, :true) end)
    new_set = MapSet.union(new_up, old_up)
    GenEvent.notify(GenServerring.Events, {:new_up_set, old_up, new_set})
    {:registered_name, name} = Process.info(self(), :registered_name)
    callback.handle_ring_change({MapSet.to_list(new_set), name, :gossip})
    new_set
  end

  defp notify_node_set(set, set, _), do: :nothingtodo
  defp notify_node_set(old_set, _, new_set) do
    GenEvent.notify(GenServerring.Events,
      {:new_node_set, get_set(old_set), get_set(new_set)})
      File.write!(ring_path, new_set |> :erlang.term_to_binary)
  end

  defp notify_payload(payload, payload, _), do: :no_payload_change
  defp notify_payload(_, _, ring), do: ring.payload

  defp ring_path,
    do: "#{Application.get_env(:gen_serverring, :data_dir, "./data")}/ring"

  defp up_set_reconcile([], _), do: :nothingtodo
  defp up_set_reconcile([n], up_set) do
    case MapSet.member?(up_set, n) do
      true -> :nothingtodo
      false -> Node.monitor(n, true)
    end
  end

  defp ring_reconcile([n], gossip, ring) do
    case contain?(ring.forced_down, n) do
      true -> {:noreply, ring} # must ignore gossips from forced_down nodes
      false -> ring_reconcile([], gossip, ring)
    end
  end
  defp ring_reconcile([], gossip, ring) do
    {ring, payload} = update_ring(ring, gossip)
    case payload do
      :no_payload_change -> :nothingtodo
      _ -> ring.callback.handle_state_change(payload)
    end
    {:noreply, ring}
  end
end

defmodule GenServerring.App do
  use Application

  def start(type, []) do
    name = Application.get_env(:gen_serverring, :name, :demo_ring)
    callback = Application.get_env(:gen_serverring, :callback, Demo)
    start(type, [{name, callback}])
  end
  def start(_type, args),
    do: Supervisor.start_link(GenServerring.App.Sup, args)

  defmodule Sup do
    use Supervisor

    def init(:no_child) do
      # just start a GenEvent, no event_handler are defined, add yours if you
      # need some
      event_name = GenServerring.Events
      child = [worker(:gen_event, [{:local, event_name}], id: event_name)]
      supervise(child, strategy: :one_for_one)
    end
    def init(arg) do
      event_name = GenServerring.Events
      children =
        [worker(:gen_event, [{:local, event_name}], id: event_name),
         worker(GenServerring, arg)]
      supervise(children, strategy: :one_for_one)
    end
  end
end

defmodule GenServerring do
  use GenServer
  require Crdtex.Set

  defstruct(
    node_set: Crdtex.Set.new,
    up_set: MapSet.new,
    forced_down: Crdtex.Set.new,
    payload: nil,
    counter: 0,
    callback: nil)

  defmacro __using__(_), do: []

  def start_link({name, callback}) do
    {:ok, payload} = callback.init([])
    GenServer.start_link(__MODULE__, {payload, callback}, [{:name, name}])
  end

  def init({payload, callback}) do
    :erlang.send_after(1_000, self(), :send_gossip)
    case File.read(ring_path) do
      {:ok, bin} ->
        set = :erlang.binary_to_term(bin)
        monitor(get_set(set))
        # should we call callback.handle_ring_change in such a situation ?
        {:ok, gen_ring(set, MapSet.new(get_set(set)), payload, 0, callback)}
      _ ->
        set = Crdtex.Set.new
        {:ok, set} = add(set, {node(), 1}, node())
        {:ok, gen_ring(set, MapSet.new([node()]), payload, 1, callback)}
    end
  end

  # payload management, mimicking a GenServer
  def call(server, action), do: GenServer.call(server, action)

  def cast(server, action), do: GenServer.cast(server, action)

  def reply(client, term), do: GenServer.reply(client, term)

  # cluster_management
  # we have to stop all nodes
  def stop({name, n} = server, reason \\ :normal, timeout \\ :infinity) do
    {:ok, ring} = get_ring(server)
    others = ring.up_set |> MapSet.delete(n)
    Enum.each(others, fn(n) -> GenServer.stop({name, n}, reason, timeout) end)
    Genserver.stop(server, reason, timeout)
  end

  def add_node(server, node) when is_binary(node) do
    add_node(server, :"#{node}")
  end
  def add_node(server, node) when is_atom(node) do
    GenServer.cast(server, {:add_node, node})
  end

  def del_node(server, node) when is_binary(node) do
    del_node(server, :"#{node}")
  end
  def del_node(server, node) when is_atom(node) do
    GenServer.cast(server, {:del_node, node})
  end

  # forbid the use of the given node (no gossip sent to it, gossip from it
  # ignored) but it might still be up, it will continue to be monitored as long
  # as it is up.
  def force_down(server, n), do:  GenServer.call(server, {:forced_down, n})

  # remove a node of the forced_down state
  def unforce_down(server, n), do:  GenServer.call(server, {:unforced_down, n})

  def all(server), do: GenServer.call(server, :get_all)
  def up(server), do: GenServer.call(server, :get_up)

  # classic GenServer callbacks
  def handle_call(:get_all, _, ring), do: {:reply, get_set(ring.node_set), ring}
  def handle_call(:get_up, _, ring) do
    {:reply, MapSet.to_list(up_nodes(ring)), ring}
  end
  def handle_call(:get_ring, _, ring), do: {:reply, {:ok, ring}, ring}
  def handle_call({:forced_down, n}, _, ring) do
    counter = ring.counter + 1
    {:ok, forced_down} = add(ring.forced_down, {node(), counter}, n)
    {:reply, get_set(forced_down),
      %{ring | forced_down: forced_down, counter: counter}}
  end
  def handle_call({:unforced_down, n}, _, ring) do
    counter = ring.counter + 1
    {:ok, forced_down} = delete(ring.forced_down, {node(), counter}, n)
    up_set = ring.up_set
    up_set =
      case MapSet.member?(up_set, n) do
        true -> up_set
        false ->
          Node.monitor(n, true)
          MapSet.put(up_set, n)
      end
    {:reply, get_set(forced_down),
      %{ring | forced_down: forced_down, up_set: up_set, counter: counter}}
  end
  def handle_call(other, from, ring) do
    payload = ring.payload
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_call(other, from, payload) do
      {:reply, reply, new} -> {:reply, reply, up_load.(new)}
      {:reply, reply, new, timeout} -> {:reply, reply, up_load.(new), timeout}
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, reply, new} -> {:stop, reason, reply, up_load.(new)}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  def handle_cast({:reconcile, gossip}, ring) do
    from_node = gossip.from_node
    up_set_reconcile(from_node, ring.up_set)
    ring_reconcile(from_node, gossip, ring)
  end
  def handle_cast({:add_node, n}, ring) do
    case contain?(ring.node_set, n) do
      true ->
        {:noreply, ring}
      false ->
        new_up_set = MapSet.put(ring.up_set, n)
        {:ok, new_node_set} = add(ring.node_set, {node(), ring.counter + 1}, n)
        {ring, _} =
          update_ring(%GenServerring{ring | up_set: new_up_set},
           %{node_set: new_node_set, payload: ring.payload, from_node: [],
             forced_down: ring.forced_down})
        {:noreply, ring}
    end
  end
  def handle_cast({:del_node, n}, ring) do
    case contain?(ring.node_set, n) do
      false ->
        {:noreply, ring}
      true ->
        new_up_set = MapSet.delete(ring.up_set, n)
        {:ok, new_node_set} =
          delete(ring.node_set, {node(), ring.counter + 1}, n)
        {ring, _} =
         update_ring(%GenServerring{ring | up_set: new_up_set},
           %{node_set: new_node_set, payload: ring.payload, from_node: [],
             forced_down: ring.forced_down})
        {:noreply, ring}
    end
  end
  def handle_cast(other, ring) do
    payload = ring.payload
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_cast(other, payload) do
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  def handle_info(:send_gossip, %GenServerring{node_set: node_set} = ring) do
    :erlang.send_after(1_000, self(), :send_gossip)
    if not contain?(node_set, node()) do
      :erlang.send_after(5_000, self(), :halt_node)
    end
    case up_nodes(ring) |> MapSet.delete(node()) |> MapSet.to_list do
      [] -> {:noreply, ring}
      active_nodes ->
        {:registered_name, name} = Process.info(self(), :registered_name)
        random_node =
          Enum.at(active_nodes, :random.uniform(length(active_nodes)) - 1)
        GenServer.cast(
          {name, random_node},
          {:reconcile,
            %{node_set: node_set,
              forced_down: ring.forced_down,
              payload: ring.payload,
              from_node: [node()]}})
      {:noreply, ring}
    end
  end
  def handle_info({:nodedown, n}, %GenServerring{up_set: up_set} = ring) do
    new_up_set =
      case MapSet.member?(up_set, n) do
        true ->
          set = MapSet.delete(up_set, n)
          {:registered_name, name} = Process.info(self(), :registered_name)
          ring.callback.handle_ring_change({MapSet.to_list(set), name,
            :nodedown})
          set
        false -> up_set
      end
    {:noreply, %{ring | up_set: new_up_set}}
  end
  def handle_info(:halt_node, s) do
    File.rm(ring_path)
    :init.stop()
    {:noreply, s}
  end
  def handle_info(other, ring) do
    up_load = fn(load) -> update_payload(ring, load) end
    case ring.callback.handle_info(other, ring.payload) do
      {:noreply, new} -> {:noreply, up_load.(new)}
      {:noreply, new, timeout} -> {:noreply, up_load.(new), timeout}
      {:stop, reason, new} -> {:stop, reason, up_load.(new)}
    end
  end

  # small utilities functions
  defp value(payload), do: Crdtex.value(payload)

  defp get_set(set), do: Crdtex.value(set)

  defp contain?(set, e), do: Crdtex.value(set, {:contains, e})

  defp add(set, actor, e), do: Crdtex.update(set, actor, {:add, e})

  defp delete(set, actor, e), do: Crdtex.update(set, actor, {:remove, e})

  defp merge(nil, crdt), do: crdt
  defp merge(crdt, crdt), do: crdt
  defp merge(crdt1, crdt2), do: Crdtex.merge(crdt1, crdt2)

  defp get_ring(server), do: GenServer.call(server, :get_ring)

  defp up_nodes(ring) do
    MapSet.difference(ring.up_set, MapSet.new(get_set(ring.forced_down)))
  end

  defp monitor(list) do
    list = List.delete(list, node())
    Enum.each(list, fn(n) -> Node.monitor(n, :true) end)
  end

  defp gen_ring(set, up, payload, counter, callback) do
    gen_ring(set, up, payload, counter, callback, Crdtex.Set.new)
  end
  defp gen_ring(set, up, payload, counter, callback, forced_down) do
    %GenServerring{node_set: set, up_set: up, forced_down: forced_down,
      payload: payload, counter: counter, callback: callback}
  end

  defp update_payload(ring, payload) do
    {ring, _} =
      update_ring(ring,
        %{node_set: ring.node_set, payload: payload, from_node: [],
          forced_down: ring.forced_down})
    ring
  end

  defp update_ring(ring, changes) do
    merged_node_set = merge(ring.node_set, changes.node_set)
    merged_payload = merge(ring.payload, changes.payload)
    merged_forced_down = merge(ring.forced_down, changes.forced_down)
    updated_counter = update_counter(merged_node_set, merged_payload, ring)

    up_set =
      notify_up_set(get_set(ring.node_set), get_set(merged_node_set),
        ring.up_set, changes.from_node, ring.callback)
    notify_node_set(ring.node_set, merged_node_set, changes.node_set)

    old_payload = ring.payload
    ring =
      gen_ring(merged_node_set, up_set, merged_payload, updated_counter,
        ring.callback, merged_forced_down)
    {ring, notify_payload(value(old_payload), value(changes.payload), ring)}
  end

  defp update_counter(merged_node_set, merged_payload, old_ring) do
    old_nodes = old_ring.node_set
    old_payload = old_ring.payload
    case {merged_node_set, merged_payload} do
      {^old_nodes, ^old_payload} -> old_ring.counter
      _ -> old_ring.counter + 1
    end
  end

  defp notify_up_set(set, set, old_up, [], _), do: old_up
  defp notify_up_set(set, set, old_up, [n], callback) do
    case MapSet.member?(old_up, n) do
      true -> old_up
      false -> notify_up_set(set, set, MapSet.put(old_up, n), callback)
    end
  end
  defp notify_up_set(old, merged, up, [], callback) do
    notify_up_set(old, merged, up, callback)
  end
  defp notify_up_set(old, merged, up, [n], callback) do
    notify_up_set(old, merged, MapSet.put(up, n), callback)
  end

  defp notify_up_set(old_set, merged_set, old_up, callback) do
    new_up = MapSet.difference(MapSet.new(merged_set), MapSet.new(old_set))
    Enum.each(new_up, fn(n) -> Node.monitor(n, :true) end)
    new_set = MapSet.union(new_up, old_up)
    GenEvent.notify(GenServerring.Events, {:new_up_set, old_up, new_set})
    {:registered_name, name} = Process.info(self(), :registered_name)
    callback.handle_ring_change({MapSet.to_list(new_set), name, :gossip})
    new_set
  end

  defp notify_node_set(set, set, _), do: :nothingtodo
  defp notify_node_set(old_set, _, new_set) do
    GenEvent.notify(GenServerring.Events, {:new_node_set, get_set(old_set), get_set(new_set)})
      File.write!(ring_path, new_set |> :erlang.term_to_binary)
  end

  defp notify_payload(payload, payload, _), do: :no_payload_change
  defp notify_payload(_, _, ring), do: ring.payload

  defp ring_path,
    do: "#{Application.get_env(:gen_serverring, :data_dir, "./data")}/ring"

  defp up_set_reconcile([], _), do: :nothingtodo
  defp up_set_reconcile([n], up_set) do
    case MapSet.member?(up_set, n) do
      true -> :nothingtodo
      false -> Node.monitor(n, true)
    end
  end

  defp ring_reconcile([n], gossip, ring) do
    case contain?(ring.forced_down, n) do
      true -> {:noreply, ring} # must ignore gossips from forced_down nodes
      false -> ring_reconcile([], gossip, ring)
    end
  end
  defp ring_reconcile([], gossip, ring) do
    {ring, payload} = update_ring(ring, gossip)
    case payload do
      :no_payload_change -> :nothingtodo
      _ -> ring.callback.handle_state_change(payload)
    end
    {:noreply, ring}
  end
end

defmodule GenServerring.App do
  use Application

  def start(type, []) do
    name = Application.get_env(:gen_serverring, :name, :demo_ring)
    callback = Application.get_env(:gen_serverring, :callback, Demo)
    start(type, [{name, callback}])
  end
  def start(_type, args) do
    Supervisor.start_link(GenServerring.App.Sup, args)
  end

  defmodule Sup do
    use Supervisor

    def init(:no_child) do
      # just start a GenEvent, no event_handler are defined, add yours if you
      # need some
      event_name = GenServerring.Events
      child = [worker(:gen_event, [{:local, event_name}], id: event_name)]
      supervise(child, strategy: :one_for_one)
    end
    def init(arg) do
      event_name = GenServerring.Events
      children =
        [worker(:gen_event, [{:local, event_name}], id: event_name),
         worker(GenServerring, arg)]
      supervise(children, strategy: :one_for_one)
    end
  end
end


fix github hook

fix github hook

defmodule GithubWebhookPlug do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, options) do
    mount = Dict.get(options, :mount)
    case conn.path_info do
      [^mount] -> github_api(conn, options)
      _ -> conn
    end
    # if not mount matches => return
    # if no signature => error
    # if signature doesn't match => error
    # done.
  end

  def github_api(conn, options) do
    key = Dict.get(options, :secret)
    {:ok, body, _} = read_body(conn)
    signature = case get_req_header(conn, "x-hub-signature") do
      ["sha1=" <> signature  | []] ->
        {:ok, signature} = Base.decode16(signature, case: :lower)
        signature
      x -> x
    end
    hmac = :crypto.hmac(:sha, key, body)
    case hmac do
      ^signature ->
        params = Poison.decode!(body)
        name = params["repository"]["name"]
        GenServer.cast(JobManager, {:clone, name, params})
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(200, "Hello world")
        |> halt
      _ ->
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(401, "Not Authorized")
        |> halt
    end
  end
end

defmodule GithubWebhookPlug do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, options) do
    mount = Dict.get(options, :mount)
    case hd(conn.path_info) do
      ^mount -> github_api(conn, options)
      _ -> conn
    end
    # if not mount matches => return
    # if no signature => error
    # if signature doesn't match => error
    # done.
  end

  def github_api(conn, options) do
    key = Dict.get(options, :secret)
    {:ok, body, _} = read_body(conn)
    signature = case get_req_header(conn, "x-hub-signature") do
      ["sha1=" <> signature  | []] ->
        {:ok, signature} = Base.decode16(signature, case: :lower)
        signature
      x -> x
    end
    hmac = :crypto.hmac(:sha, key, body)
    case hmac do
      ^signature ->
        params = Poison.decode!(body)
        name = params["repository"]["name"]
        GenServer.cast(JobManager, {:clone, name, params})
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(200, "Hello world")
        |> halt
      _ ->
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(401, "Not Authorized")
        |> halt
    end
  end
end