Refactor WebHook.Handler test

Refactor WebHook.Handler test

defmodule Poxa.WebHook.HandlerTest do
  use ExUnit.Case
  alias Poxa.Channel
  alias Poxa.WebHook.EventTable
  import :meck
  import Poxa.WebHook.Handler

  @table_name :web_hook_events

  setup_all do
    :application.set_env(:poxa, :app_secret, "secret")
    :application.set_env(:poxa, :app_key, "app_key")
    :application.set_env(:poxa, :web_hook, "web_hook_url")

    new Channel

    on_exit fn -> unload end
    :ok
  end

  setup do
    case :ets.info(@table_name) do
      :undefined -> EventTable.init
      _ -> :ets.delete_all_objects(@table_name)
    end
    :ok
  end

  test "connected is not saved" do
    assert {:ok, []} == handle_event(%{event: :connected}, [])
    assert EventTable.all == []
  end

  test "disconnected is not saved" do
    expect Channel, :occupied?, fn
      "channel_1" -> false
      "channel_2" -> true
    end
    assert {:ok, []} == handle_event(%{event: :disconnected, channels: ~w(channel_1 channel_2)}, [])
    assert EventTable.all == []
  end

  test "subscribed is not saved" do
    expect Channel, :subscription_count, fn _ -> 1 end
    assert {:ok, []} == handle_event(%{event: :subscribed, channel: "my_channel"}, [])
    assert EventTable.all == []
  end

  test "unsubscribed is not saved" do
    expect Channel, :occupied?, fn _ -> false end
    assert {:ok, []} == handle_event(%{event: :unsubscribed, channel: "my_channel"}, [])
    assert EventTable.all == []
  end

  test "channel_vacated is saved and delayed" do
    assert {:ok, []} == handle_event(%{event: :channel_vacated, channel: "my_channel"}, [])
    assert EventTable.all == [
      %{channel: "my_channel", name: "channel_vacated"}
    ]
    assert {_, []} = EventTable.ready
  end

  test "channel_occupied is saved" do
    assert {:ok, []} == handle_event(%{event: :channel_occupied, channel: "my_channel"}, [])
    assert EventTable.all == [
      %{channel: "my_channel", name: "channel_occupied"}
    ]
    assert {_, [_]} = EventTable.ready
  end

  test "client_event_message is saved" do
    assert {:ok, []} == handle_event(%{event: :client_event_message, socket_id: "socket_id", channels: ~w(channel_1 channel_2), name: "my_event", data: "data"}, [])
    expected = [
      %{channel: "channel_1", name: "client_event", event: "my_event", data: "data", socket_id: "socket_id"},
      %{channel: "channel_2", name: "client_event", event: "my_event", data: "data", socket_id: "socket_id"},
    ] |> Enum.into(MapSet.new)
    assert MapSet.equal?(expected, Enum.into(EventTable.all, MapSet.new))
    assert {_, [_, _]} = EventTable.ready
  end

  test "member_added is saved" do
    assert {:ok, []} == handle_event(%{event: :member_added, channel: "channel", user_id: "user_id"}, [])
    assert EventTable.all == [
      %{channel: "channel", name: "member_added", user_id: "user_id"},
    ]
    assert {_, [_]} = EventTable.ready
  end

  test "member_removed is saved and delayed" do
    assert {:ok, []} == handle_event(%{event: :member_removed, channel: "channel", user_id: "user_id"}, [])
    assert EventTable.all == [
      %{channel: "channel", name: "member_removed", user_id: "user_id"},
    ]
    assert {_, []} = EventTable.ready
  end
end

defmodule Poxa.WebHook.HandlerTest do
  use ExUnit.Case
  alias Poxa.Channel
  alias Poxa.WebHook.EventTable
  import :meck
  import Poxa.WebHook.Handler

  @table_name :web_hook_events

  setup_all do
    :application.set_env(:poxa, :app_secret, "secret")
    :application.set_env(:poxa, :app_key, "app_key")
    :application.set_env(:poxa, :web_hook, "web_hook_url")

    new Channel

    on_exit fn -> unload end
    :ok
  end

  setup do
    case :ets.info(@table_name) do
      :undefined -> EventTable.init
      _ -> :ets.delete_all_objects(@table_name)
    end
    :ok
  end

  test "connected is not saved" do
    assert {:ok, []} == handle_event(%{event: :connected}, [])
    assert EventTable.all == []
  end

  test "disconnected is not saved" do
    expect Channel, :occupied?, fn
      "channel_1" -> false
      "channel_2" -> true
    end
    assert {:ok, []} == handle_event(%{event: :disconnected, channels: ~w(channel_1 channel_2)}, [])
    assert EventTable.all == []
  end

  test "subscribed is not saved" do
    expect Channel, :subscription_count, fn _ -> 1 end
    assert {:ok, []} == handle_event(%{event: :subscribed, channel: "my_channel"}, [])
    assert EventTable.all == []
  end

  test "unsubscribed is not saved" do
    expect Channel, :occupied?, fn _ -> false end
    assert {:ok, []} == handle_event(%{event: :unsubscribed, channel: "my_channel"}, [])
    assert EventTable.all == []
  end

  test "channel_vacated is saved and delayed" do
    assert {:ok, []} == handle_event(%{event: :channel_vacated, channel: "my_channel"}, [])
    assert EventTable.all == [
      %{channel: "my_channel", name: "channel_vacated"}
    ]
    assert {_, []} = EventTable.ready
  end

  test "channel_occupied is saved" do
    assert {:ok, []} == handle_event(%{event: :channel_occupied, channel: "my_channel"}, [])
    assert EventTable.all == [
      %{channel: "my_channel", name: "channel_occupied"}
    ]
    assert {_, [_]} = EventTable.ready
  end

  test "client_event_message is saved" do
    assert {:ok, []} == handle_event(%{event: :client_event_message, socket_id: "socket_id", channels: ~w(channel_1 channel_2), name: "my_event", data: "data"}, [])
    assert EventTable.all == [
      %{channel: "channel_1", name: "client_event", event: "my_event", data: "data", socket_id: "socket_id"},
      %{channel: "channel_2", name: "client_event", event: "my_event", data: "data", socket_id: "socket_id"},
    ]
    assert {_, [_, _]} = EventTable.ready
  end

  test "member_added is saved" do
    assert {:ok, []} == handle_event(%{event: :member_added, channel: "channel", user_id: "user_id"}, [])
    assert EventTable.all == [
      %{channel: "channel", name: "member_added", user_id: "user_id"},
    ]
    assert {_, [_]} = EventTable.ready
  end

  test "member_removed is saved and delayed" do
    assert {:ok, []} == handle_event(%{event: :member_removed, channel: "channel", user_id: "user_id"}, [])
    assert EventTable.all == [
      %{channel: "channel", name: "member_removed", user_id: "user_id"},
    ]
    assert {_, []} = EventTable.ready
  end
end


Removing async tasks

Removing async tasks

defmodule DevelopmentSetup.Packages.Atom do
  @packages ~w(
    atom-beautify
    atom-elixir
    autocomplete-elixir
    file-icons
    file-types
    git-plus
    merge-conflicts
    minimap
    html-entities
    language-elixir
    language-vue
    linter
    linter-csslint
    linter-elixir-credo
    linter-erb
    linter-jshint
    linter-php
    linter-rubocop
    linter-ruby
    linter-scss-lint
    linter-shellcheck
    merge-conflicts
    phoenix-snippets
    pretty-json
    project-manager
    svg-preview
    todo
    rails-snippets
    remote-atom
    title-case
    xml-formatter
    atom-material-ui
    atom-material-syntax
    atom-material-syntax-dark
    atom-material-syntax-light
  )

  def install(_force) do
    if File.exists?("/Applications/Atom.app/Contents/Resources/app/apm/bin/apm") do
      IO.puts "Installing Atom packages..."

      @packages
      |> Enum.map(&install_package(&1))

      IO.puts "Done."
    else
      IO.puts "Couldn't find the Atom package manager"
    end
  end

  defp install_package(package) do
    if !File.exists?(Path.join(System.user_home, ".atom/packages/#{package}")) do
      IO.puts "    Atom - Installing #{package}"
      System.cmd("/Applications/Atom.app/Contents/Resources/app/apm/bin/apm", [package])
    end
  end
end

defmodule DevelopmentSetup.Packages.Atom do
  @packages ~w(
    atom-beautify
    atom-elixir
    autocomplete-elixir
    file-icons
    file-types
    git-plus
    merge-conflicts
    minimap
    html-entities
    language-elixir
    language-vue
    linter
    linter-csslint
    linter-elixir-credo
    linter-erb
    linter-jshint
    linter-php
    linter-rubocop
    linter-ruby
    linter-scss-lint
    linter-shellcheck
    merge-conflicts
    phoenix-snippets
    pretty-json
    project-manager
    svg-preview
    todo
    rails-snippets
    remote-atom
    title-case
    xml-formatter
    atom-material-ui
    atom-material-syntax
    atom-material-syntax-dark
    atom-material-syntax-light
  )

  def install(_force) do
    if File.exists?("/Applications/Atom.app/Contents/Resources/app/apm/bin/apm") do
      IO.puts "Installing Atom packages..."

      @packages
      |> Enum.map(&Task.async(fn ->
           install_package(&1)
         end))
      |> Enum.map(&Task.await(&1))

      IO.puts "Done."
    else
      IO.puts "Couldn't find the Atom package manager"
    end
  end

  defp install_package(package) do
    if !File.exists?(Path.join(System.user_home, ".atom/packages/#{package}")) do
      IO.puts "    Atom - Installing #{package}"
      System.cmd("/Applications/Atom.app/Contents/Resources/app/apm/bin/apm", [package])
    end
  end
end


release session fixes

release session fixes

defmodule RaxxSession.MixProject do
  use Mix.Project

  def project do
    [
      app: :raxx_session,
      version: "0.2.5",
      elixir: "~> 1.7",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      description: description(),
      docs: [extras: ["README.md"], main: "readme"],
      package: package()
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:raxx, "~> 1.0"},
      {:cookie, "~> 0.1.1"},
      {:plug_crypto, "~> 1.0"},
      {:plug, "~> 1.8", only: :test},
      {:ex_doc, ">= 0.0.0", only: :dev}
    ]
  end

  defp description do
    """
    Manage HTTP cookies and storage for persistent client sessions.
    """
  end

  defp package do
    [
      maintainers: ["Peter Saxton"],
      licenses: ["Apache 2.0"],
      links: %{
        "GitHub" => "https://github.com/crowdhailer/raxx/tree/master/extensions/raxx_session"
      }
    ]
  end
end

defmodule RaxxSession.MixProject do
  use Mix.Project

  def project do
    [
      app: :raxx_session,
      version: "0.2.4",
      elixir: "~> 1.7",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      description: description(),
      docs: [extras: ["README.md"], main: "readme"],
      package: package()
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:raxx, "~> 1.0"},
      {:cookie, "~> 0.1.1"},
      {:plug_crypto, "~> 1.0"},
      {:plug, "~> 1.8", only: :test},
      {:ex_doc, ">= 0.0.0", only: :dev}
    ]
  end

  defp description do
    """
    Manage HTTP cookies and storage for persistent client sessions.
    """
  end

  defp package do
    [
      maintainers: ["Peter Saxton"],
      licenses: ["Apache 2.0"],
      links: %{
        "GitHub" => "https://github.com/crowdhailer/raxx/tree/master/extensions/raxx_session"
      }
    ]
  end
end


Added fix to support latest changes in orientDB on the string default value

defmodule MarcoPolo.Protocol.RecordSerialization do
  @moduledoc false

  # This module implements the serialization/deserialization protocol described
  # here:
  # http://orientdb.com/docs/last/Record-Schemaless-Binary-Serialization.html. It's
  # a little bit of a mess.

  require Record
  Record.defrecordp :field, [:name, :ptr, :type]
  @typep field :: {:field, binary, integer, atom}

  @typep schema :: HashDict.t | %{}

  defmodule State do
    @type t :: %__MODULE__{whole_data: binary, schema: HashDict.t | %{}}
    defstruct schema: %{}, whole_data: nil
  end

  alias MarcoPolo.Document
  alias MarcoPolo.RID
  alias MarcoPolo.Protocol.Utils

  import MarcoPolo.Protocol.Protobuf

  @simple_types ~w(boolean
                   short
                   int
                   long
                   float
                   double
                   string
                   binary
                   date
                   datetime
                   decimal
                   link
                   link_list
                   link_set
                   link_map
                   link_bag)a

  @embedded_types ~w(embedded
                     embedded_list
                     embedded_set
                     embedded_map)a

  @doc """
  Decodes a binary-serialized record into a `MarcoPolo.Record` struct.

  This function decodes the bytes representing a record into a
  `MarcoPolo.Record` struct. The record is assumed to be serialized with the
  `ORecordSerializerBinary` serialization. `data` is the full data about the
  record, it has no exceeding data and it's not incomplete. Note that `data`
  represents the bytes for the record content, without the leading int for the
  length of the byte array you would expect from OrientDB's binary
  protocol. This happens because this function is usually called from the parser
  that parsed the byte array. This also means we don't have to care about
  possibly incomplete parts of the serialized records because that would have
  been detected by the parser (that knows the size of the serialized binary).
  """
  @spec decode(binary, Dict.t) :: Document.t | :unknown_property_id
  def decode(data, schema \\ %{}) do
    state = %State{whole_data: data, schema: schema}

    <<_serialization_version, rest :: binary>> = data

    # OrientDB sometimes sends stuff after a record that they use to keep track
    # of updates and other things. Let's ignore this stuff and hope everything
    # goes fine, shall we?
    case decode_embedded(rest, state) do
      {record, _cruft} ->
        record
      :unknown_property_id ->
        :unknown_property_id
    end
  end

  @doc """
  Serializes a given record using the schemaless serialization protocol.

  The record is serialized using the `ORecordSerializerBinary`
  serialization. This function always returns iodata that can be converted to
  binary using `IO.iodata_to_binary/1`.

  This function is the "dual" of `decode/2`, so this is generally true:

      decode(encode(record)) = record

  """
  @spec encode(MarcoPolo.record) :: iodata
  def encode(%Document{} = record) do
    # 0 is the serialization version (as a byte), not the record version.
    [0, encode_embedded(record, 1)]
  end

  # Decodes a document (ODocument). This ODocument can be a "top-level" document
  # or an "embedded" type, since the leading serialization version byte is not
  # decoded here (but in `decode/2`).
  defp decode_embedded(data, %State{} = state) do
    {class_name, rest} = decode_type(data, :string)
    class_name = nullify_empty_string(class_name)

    case decode_header(rest, state) do
      {[], rest} ->
        {%Document{class: class_name, fields: %{}}, rest}
      {field_definitions, _rest} ->
        {fields, rest} = decode_fields(field_definitions, state)
        {%Document{class: class_name, fields: fields}, rest}
      :unknown_property_id ->
        :unknown_property_id
    end
  end

  # Decodes the header of this record (which contains field definitions and
  # "pointers" to the corresponding data in the rest of the binary. Returns a
  # tuple with a list of fields as the first element and the non-header data as
  # the second element.
  @spec decode_header(binary, State.t, [field]) :: [field]
  defp decode_header(data, %State{} = state, acc \\ []) do
    # If `i` is positive, that means the next field definition is a "named
    # field" and `i` is the length of the field's name. If it's negative, it
    # represents the property id of a property. If it's 0, it signals the end of
    # the header segment.
    case decode_zigzag_varint(data) do
      {0, rest} ->
        {Enum.reverse(acc), rest}
      {i, rest} when i < 0 ->
        case decode_property_definition(rest, i, state.schema) do
          {field, rest}        -> decode_header(rest, state, [field|acc])
          :unknown_property_id -> :unknown_property_id
        end
      {i, _} when i > 0 ->
        {field, rest} = decode_field_definition(data)
        decode_header(rest, state, [field|acc])
    end
  end

  @spec decode_property_definition(binary, integer, schema) :: {field, binary} | :unknown_property_id
  defp decode_property_definition(data, encoded_id, schema) do
    # That's how you decode property ids.
    id = - encoded_id - 1

    case Dict.fetch(schema.global_properties, id) do
      {:ok, {name, type_as_string}} ->
        {ptr, rest} = decode_data_ptr(data)
        field       = field(name: name, type: string_to_type(type_as_string), ptr: ptr)
        {field, rest}
      :error ->
        :unknown_property_id
    end
  end

  @spec decode_field_definition(binary) :: {field, binary}
  defp decode_field_definition(data) do
    {name, rest}             = decode_type(data, :string)
    {ptr, rest}              = decode_data_ptr(rest)
    <<type, rest :: binary>> = rest

    {field(name: name, type: int_to_type(type), ptr: ptr), rest}
  end

  @spec decode_fields([field], State.t) :: {%{}, binary}
  defp decode_fields(field_definitions, %State{} = state) when is_list(field_definitions) do
    fields = Enum.map(field_definitions, &decode_field(&1, state))
    {fields_to_map(fields), get_rest_from_list_of_fields(fields)}
  end

  @spec decode_field(field, State.t) :: {{binary, term}, binary}
  defp decode_field(field, state)

  # A 0 pointer means the field is null.
  defp decode_field(field(name: name, ptr: 0), %State{}) do
    {{name, nil}, <<>>}
  end

  defp decode_field(field(name: name, type: type, ptr: ptr), %State{} = state) do
    {value, rest} = decode_type(pointed_data(state.whole_data, ptr), type, state)
    {{name, value}, rest}
  end

  # The pointer to the data is just a signed int32.
  defp decode_data_ptr(data) do
    <<data_ptr :: 32-signed, rest :: binary>> = data
    {data_ptr, rest}
  end

  # Made public for testing.
  @doc false
  @spec decode_type(binary, atom) :: {term, binary}
  def decode_type(pointed_data, type) when type in @simple_types do
    decode_simple_type(pointed_data, type)
  end

  # Made public for testing.
  @doc false
  @spec decode_type(binary, atom, State.t) :: {term, binary}
  def decode_type(pointed_data, type, _state) when type in @simple_types,
    do: decode_simple_type(pointed_data, type)
  def decode_type(pointed_data, type, state) when type in @embedded_types,
    do: decode_embedded_type(pointed_data, type, state)

  @spec decode_simple_type(binary, atom) :: {term, binary}
  defp decode_simple_type(pointed_data, type)

  defp decode_simple_type(<<0>> <> rest, :boolean), do: {false, rest}
  defp decode_simple_type(<<1>> <> rest, :boolean), do: {true, rest}

  defp decode_simple_type(pointed_data, type) when type in [:short, :int, :long] do
    decode_zigzag_varint(pointed_data)
  end

  defp decode_simple_type(pointed_data, :float) do
    <<float :: 32-float, rest :: binary>> = pointed_data
    {float, rest}
  end

  defp decode_simple_type(pointed_data, :double) do
    <<double :: 64-float, rest :: binary>> = pointed_data
    {double, rest}
  end

  # Strings and binaries are encoded/decoded in the exact same way.
  defp decode_simple_type(pointed_data, type) when type in [:string, :binary] do
    {len, rest} = decode_zigzag_varint(pointed_data)
    <<str :: bytes-size(len), rest :: binary>> = rest
    {str, rest}
  end

  defp decode_simple_type(pointed_data, :date) do
    {days, rest} = decode_zigzag_varint(pointed_data)
    days = :calendar.date_to_gregorian_days(1970, 1, 1) + days
    {y, m, d} = :calendar.gregorian_days_to_date(days)
    {%MarcoPolo.Date{year: y, month: m, day: d}, rest}
  end

  defp decode_simple_type(pointed_data, :datetime) do
    {msecs_from_epoch, rest} = decode_simple_type(pointed_data, :long)
    secs_from_epoch = div(msecs_from_epoch, 1000)
    msec = rem(msecs_from_epoch, 1000)
    epoch = :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})

    total_secs = epoch + secs_from_epoch
    {{year, month, day}, {hour, min, sec}} = :calendar.gregorian_seconds_to_datetime(total_secs)

    {%MarcoPolo.DateTime{year: year, month: month, day: day,
                         hour: hour, min: min, sec: sec, msec: msec},
     rest}
  end

  defp decode_simple_type(data, :decimal) do
    <<scale :: 32, value_size :: 32, rest :: binary>>         = data
    <<value :: big-size(value_size)-unit(8), rest :: binary>> = rest

    value = value / round(:math.pow(10, scale))
    {Decimal.new(value), rest}
  end

  defp decode_simple_type(data, :link) do
    {cluster_id, rest} = decode_zigzag_varint(data)
    {position, rest} = decode_zigzag_varint(rest)
    {%RID{cluster_id: cluster_id, position: position}, rest}
  end

  defp decode_simple_type(data, :link_list) do
    {nelems, rest} = decode_zigzag_varint(data)
    {elems, rest} = Utils.reduce_n_times(nelems, rest, &decode_simple_type(&1, :link))
    {{:link_list, elems}, rest}
  end

  defp decode_simple_type(data, :link_set) do
    {{:link_list, elems}, rest} = decode_simple_type(data, :link_list)
    {{:link_set, Enum.into(elems, HashSet.new)}, rest}
  end

  defp decode_simple_type(data, :link_map) do
    {nkeys, rest} = decode_zigzag_varint(data)
    {pairs, rest} = Utils.reduce_n_times nkeys, rest, fn(<<type>> <> acc) ->
      # Only string keys are supported
      :string = int_to_type(type)

      {key, acc} = decode_simple_type(acc, :string)
      {rid, acc} = decode_simple_type(acc, :link)
      {{key, rid}, acc}
    end

    {{:link_map, Enum.into(pairs, %{})}, rest}
  end

  # 1 means "embedded" RidBag.
  defp decode_simple_type(<<1, size :: 32, rest :: binary>>, :link_bag) do
    {rids, rest} = Utils.reduce_n_times size, rest, fn(acc) ->
      <<cluster_id :: 16, position :: 64>> <> acc = acc
      {%RID{cluster_id: cluster_id, position: position}, acc}
    end

    {{:link_bag, rids}, rest}
  end

  defp decode_simple_type(<<0, _ :: binary>>, :link_bag) do
    raise MarcoPolo.Error, """
    Tree-based RidBags are not supported by MarcoPolo (yet); only embedded
    RidBags are. You can change your OrientDB server configuration to force
    OrientDB to use embedded RidBags over tree-based ones. To learn more about
    changing the server configuration, visit
    http://orientdb.com/docs/last/Configuration.html.  The setting to change is
    `ridBag.embeddedToSbtreeBonsaiThreshold`: set it to a very high value to
    ensure OrientDB uses embedded RidBags up to that number of relations. For
    example:

        <properties>
          ...
          <entry name="ridBag.embeddedToSbtreeBonsaiThreshold" value="1000000000" />
        </properties>

    Note that for this configuration to take effect for a database, that
    database must be created after this configuration is set on the server.
    """
  end

  @spec decode_embedded_type(binary, atom, State.t) :: {term, binary}
  defp decode_embedded_type(pointed_data, type, state)

  defp decode_embedded_type(pointed_data, :embedded, %State{} = state) do
    decode_embedded(pointed_data, state)
  end

  defp decode_embedded_type(pointed_data, :embedded_list, %State{} = state) do
    {nitems, rest}           = decode_zigzag_varint(pointed_data)
    <<type, rest :: binary>> = rest

    # Only ANY is supported by OrientDB at the moment.
    :any = int_to_type(type)

    Utils.reduce_n_times nitems, rest, fn(<<type>> <> acc) ->
      decode_type(acc, int_to_type(type), state)
    end
  end

  defp decode_embedded_type(pointed_data, :embedded_set, %State{} = state) do
    {elems, rest} = decode_embedded_type(pointed_data, :embedded_list, state)
    {Enum.into(elems, HashSet.new), rest}
  end

  defp decode_embedded_type(pointed_data, :embedded_map, %State{} = state) do
    decode_embedded_map(pointed_data, state)
  end

  defp decode_embedded_map(pointed_data, state) do
    case decode_map_keys(pointed_data) do
      {[], rest}    -> {%{}, rest}
      {keys, _rest} -> decode_map_values(keys, state)
    end
  end

  defp decode_map_keys(data) do
    {nkeys, rest} = decode_zigzag_varint(data)

    Utils.reduce_n_times nkeys, rest, fn(<<type>> <> acc) ->
      # For now, OrientDB only supports STRING keys.
      :string = int_to_type(type)

      {key, acc} = decode_simple_type(acc, :string)
      {ptr, acc} = decode_data_ptr(acc)
      <<type, acc :: binary>> = acc

      {field(name: key, type: int_to_type(type), ptr: ptr), acc}
    end
  end

  defp decode_map_values(keys, %State{} = state) do
    values = Enum.map(keys, &decode_field(&1, state))
    rest = get_rest_from_list_of_fields(values)
    values = fields_to_map(values)
    {values, rest}
  end

  defp encode_fields(fields, offset) when is_map(fields) do
    offset = offset + header_offset(fields)

    acc = {[], [], offset}
    {fields, values, _} = Enum.reduce fields, acc, fn({field_name, field_value}, {fs, vs, index}) ->
      encoded_value =
        if is_nil(field_value) do
          <<>>
        else
          encode_value(field_value, index)
        end

      encoded_field = encode_field_for_header(field_name, index, field_value)
      index         = index + IO.iodata_length(encoded_value)

      {[encoded_field|fs], [encoded_value|vs], index}
    end

    [Enum.reverse(fields), 0, Enum.reverse(values)]
  end

  defp header_offset(fields) do
    # The last +1 is for the `0` that signals the end of the header.
    fields
    |> Stream.map(fn({name, value}) -> encode_field_for_header(name, 0, value) end)
    |> Stream.map(&IO.iodata_length/1)
    |> Enum.sum
    |> Kernel.+(1)
  end

  defp encode_embedded(%Document{class: class, fields: fields}, offset) do
    class = if is_nil(class), do: "", else: class
    encoded_class  = encode_value(class)
    encoded_fields = encode_fields(fields, offset + IO.iodata_length(encoded_class))

    [encoded_class, encoded_fields]
  end

  defp encode_field_for_header(name, ptr, value) do
    type = infer_type(value)
    name = to_string(name)

    {ptr, type} =
      if is_nil(value) do
        {0, :boolean}
      else
        {ptr, type}
      end

    [encode_value(name), <<ptr :: 32-signed>>, type_to_int(type)]
  end

  # Encodes a value inferring its type.
  # Made public for testing.
  @doc false
  def encode_value(value, offset \\ 0)

  def encode_value({type, value}, offset),
    do: encode_type(value, type, offset)
  def encode_value(value, offset),
    do: encode_type(value, infer_type(value), offset)

  defp encode_type(value, type, offset)

  defp encode_type(true, :boolean, _offset),  do: <<1>>
  defp encode_type(false, :boolean, _offset), do: <<0>>

  defp encode_type(binary, type, _offset) when type in [:string, :binary] do
    [encode_zigzag_varint(byte_size(binary)), binary]
  end

  defp encode_type(i, type, _offset) when type in [:short, :int, :long] do
    encode_zigzag_varint(i)
  end

  defp encode_type(x, :float, _offset), do: <<x :: 32-float>>
  defp encode_type(x, :double, _offset), do: <<x :: 64-float>>

  defp encode_type(date, :date, _offset) do
     import :calendar, only: [date_to_gregorian_days: 3]
     days = date_to_gregorian_days(date.year, date.month, date.day) - date_to_gregorian_days(1970, 1, 1)
     encode_zigzag_varint(days)
  end

  defp encode_type(dt, :datetime, _offset) do
    datetime = {{dt.year, dt.month, dt.day}, {dt.hour, dt.min, dt.sec}}
    secs     = :calendar.datetime_to_gregorian_seconds(datetime)
    epoch    = :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})

    encode_type((secs - epoch) * 1000 + dt.msec, :long, 0)
  end

  defp encode_type(record, :embedded, offset) do
    encode_embedded(record, offset)
  end

  defp encode_type(list, :embedded_list, offset) do
    elems = Enum.map list, fn(el) ->
      [type_to_int(infer_type(el)), encode_value(el, offset)]
    end

    [encode_zigzag_varint(length(list)), type_to_int(:any), elems]
  end

  defp encode_type(set, :embedded_set, offset) do
    encode_type(Set.to_list(set), :embedded_list, offset)
  end

  defp encode_type(map, :embedded_map, offset) when is_map(map) do
    offset = offset + map_header_offset(map)

    {keys, values, _} = Enum.reduce map, {[], [], offset}, fn({key, value}, {ks, vs, index}) ->
      encoded_value = <<>>

      {key, index, encoded_value} =
        if is_nil(value) do
          key = [type_to_int(:string),
                 encode_value(to_string(key)),
                 <<0 :: 32-signed>>,
                 0]
          {key, index, encoded_value}
        else
          key = [type_to_int(:string),
                 encode_value(to_string(key)),
                 <<index :: 32-signed>>,
                 type_to_int(infer_type(value))]
          encoded_value = encode_value(value, index)
          index = index + IO.iodata_length(encoded_value)
          {key, index, encoded_value}
        end

      {[key|ks], [encoded_value|vs], index}
    end

    keys   = Enum.reverse(keys)
    values = Enum.reverse(values)

    nkeys = map |> map_size() |> encode_zigzag_varint()

    [nkeys, keys, values]
  end

  defp encode_type(%RID{cluster_id: id, position: pos}, :link, _offset) do
    encode_zigzag_varint(id) <> encode_zigzag_varint(pos)
  end

  defp encode_type(rids, :link_list, offset) do
    [
      encode_zigzag_varint(length(rids)),
      Enum.map(rids, &encode_type(&1, :link, offset))
    ]
  end

  defp encode_type(rids, :link_set, offset) do
    encode_type(Set.to_list(rids), :link_list, offset)
  end

  defp encode_type(rid_map, :link_map, offset) do
    keys_and_values = Enum.map rid_map, fn {k, v} ->
      [type_to_int(:string),
       encode_value(to_string(k), offset),
       encode_value(v, offset)]
    end

    [encode_zigzag_varint(map_size(rid_map)), keys_and_values]
  end

  defp encode_type(rids, :link_bag, _) when is_list(rids) do
    encoded_rids = Enum.map rids, fn(%RID{cluster_id: cluster_id, position: position}) ->
      <<cluster_id :: 16, position :: 64>>
    end

    [1, <<length(rids) :: 32>>|encoded_rids]
  end

  defp map_header_offset(map) do
    keys = Map.keys(map)

    # `6` means 4 bytes for the pointer to the data, 1 byte for the data type,
    # and 1 byte for the key type.
    nkeys       = encode_zigzag_varint(Enum.count(keys))
    key_lengths = Enum.map(keys, &(IO.iodata_length(encode_value(to_string(&1))) + 6))

    byte_size(nkeys) + Enum.sum(key_lengths)
  end

  defp pointed_data(data, position) when position > 0 and byte_size(data) > position,
    do: binary_part(data, position, byte_size(data) - position)
  defp pointed_data(data, pos),
    do: raise(ArgumentError, "position #{pos} is outside of the given binary (which is #{byte_size(data)} bytes long)")

  defp nullify_empty_string(""), do: nil
  defp nullify_empty_string(str) when is_binary(str), do: str

  defp get_rest_from_list_of_fields([_|_] = fields) do
    case get_serialized_data(fields) do
      nil -> <<>>
      {{_name, _value}, serialized_data} -> serialized_data
    end
  end

  defp get_serialized_data(fields) do
    fields
    |> Enum.reverse()
    |> Enum.drop_while(fn {{_name, val}, _rest} -> is_nil(val) end)
    |> List.first()
  end

  defp fields_to_map(fields) do
    for {name_and_value, _rest} <- fields, into: %{} do
      name_and_value
    end
  end

  defp infer_type(%HashSet{}),               do: :embedded_set
  defp infer_type(%Document{}),              do: :embedded
  defp infer_type(%RID{}),                   do: :link
  defp infer_type(%MarcoPolo.Date{}),        do: :date
  defp infer_type(%MarcoPolo.DateTime{}),    do: :datetime
  defp infer_type(%Decimal{}),               do: :decimal
  defp infer_type(val) when is_boolean(val), do: :boolean
  defp infer_type(val) when is_binary(val),  do: :string
  defp infer_type(val) when is_integer(val), do: :int
  defp infer_type(val) when is_float(val),   do: :double
  defp infer_type(val) when is_list(val),    do: :embedded_list
  defp infer_type(val) when is_map(val),     do: :embedded_map
  defp infer_type(val) when is_nil(val),     do: :boolean # irrelevant
  defp infer_type({type, _}),                do: type

  # http://orientdb.com/docs/last/Types.html
  @types [
    {:boolean, "BOOLEAN", 0},
    {:int, "INTEGER", 1},
    {:short, "SHORT", 2},
    {:long, "LONG", 3},
    {:float, "FLOAT", 4},
    {:double, "DOUBLE", 5},
    {:datetime, "DATETIME", 6},
    {:string, "STRING", 7},
    {:binary, "BINARY", 8},
    {:embedded, "EMBEDDED", 9},
    {:embedded_list, "EMBEDDEDLIST", 10},
    {:embedded_set, "EMBEDDEDSET", 11},
    {:embedded_map, "EMBEDDEDMAP", 12},
    {:link, "LINK", 13},
    {:link_list, "LINKLIST", 14},
    {:link_set, "LINKSET", 15},
    {:link_map, "LINKMAP", 16},
    {:byte, "BYTE", 17},
    {:transient, "TRANSIENT", 18},
    {:date, "DATE", 19},
    {:custom, "CUSTOM", 20},
    {:decimal, "DECIMAL", 21},
    {:link_bag, "LINKBAG", 22},
    {:any, "ANY", 23},
  ]

  for {type_name, stringified_type, type_id} <- @types do
    defp int_to_type(unquote(type_id)),             do: unquote(type_name)
    defp type_to_int(unquote(type_name)),           do: unquote(type_id)
    defp string_to_type(unquote(stringified_type)), do: unquote(type_name)
  end
end

defmodule MarcoPolo.Protocol.RecordSerialization do
  @moduledoc false

  # This module implements the serialization/deserialization protocol described
  # here:
  # http://orientdb.com/docs/last/Record-Schemaless-Binary-Serialization.html. It's
  # a little bit of a mess.

  require Record
  Record.defrecordp :field, [:name, :ptr, :type]
  @typep field :: {:field, binary, integer, atom}

  @typep schema :: HashDict.t | %{}

  defmodule State do
    @type t :: %__MODULE__{whole_data: binary, schema: HashDict.t | %{}}
    defstruct schema: %{}, whole_data: nil
  end

  alias MarcoPolo.Document
  alias MarcoPolo.RID
  alias MarcoPolo.Protocol.Utils

  import MarcoPolo.Protocol.Protobuf

  @simple_types ~w(boolean
                   short
                   int
                   long
                   float
                   double
                   string
                   binary
                   date
                   datetime
                   decimal
                   link
                   link_list
                   link_set
                   link_map
                   link_bag)a

  @embedded_types ~w(embedded
                     embedded_list
                     embedded_set
                     embedded_map)a

  @doc """
  Decodes a binary-serialized record into a `MarcoPolo.Record` struct.

  This function decodes the bytes representing a record into a
  `MarcoPolo.Record` struct. The record is assumed to be serialized with the
  `ORecordSerializerBinary` serialization. `data` is the full data about the
  record, it has no exceeding data and it's not incomplete. Note that `data`
  represents the bytes for the record content, without the leading int for the
  length of the byte array you would expect from OrientDB's binary
  protocol. This happens because this function is usually called from the parser
  that parsed the byte array. This also means we don't have to care about
  possibly incomplete parts of the serialized records because that would have
  been detected by the parser (that knows the size of the serialized binary).
  """
  @spec decode(binary, Dict.t) :: Document.t | :unknown_property_id
  def decode(data, schema \\ %{}) do
    state = %State{whole_data: data, schema: schema}

    <<_serialization_version, rest :: binary>> = data

    # OrientDB sometimes sends stuff after a record that they use to keep track
    # of updates and other things. Let's ignore this stuff and hope everything
    # goes fine, shall we?
    case decode_embedded(rest, state) do
      {record, _cruft} ->
        record
      :unknown_property_id ->
        :unknown_property_id
    end
  end

  @doc """
  Serializes a given record using the schemaless serialization protocol.

  The record is serialized using the `ORecordSerializerBinary`
  serialization. This function always returns iodata that can be converted to
  binary using `IO.iodata_to_binary/1`.

  This function is the "dual" of `decode/2`, so this is generally true:

      decode(encode(record)) = record

  """
  @spec encode(MarcoPolo.record) :: iodata
  def encode(%Document{} = record) do
    # 0 is the serialization version (as a byte), not the record version.
    [0, encode_embedded(record, 1)]
  end

  # Decodes a document (ODocument). This ODocument can be a "top-level" document
  # or an "embedded" type, since the leading serialization version byte is not
  # decoded here (but in `decode/2`).
  defp decode_embedded(data, %State{} = state) do
    {class_name, rest} = decode_type(data, :string)
    class_name = nullify_empty_string(class_name)

    case decode_header(rest, state) do
      {[], rest} ->
        {%Document{class: class_name, fields: %{}}, rest}
      {field_definitions, _rest} ->
        {fields, rest} = decode_fields(field_definitions, state)
        {%Document{class: class_name, fields: fields}, rest}
      :unknown_property_id ->
        :unknown_property_id
    end
  end

  # Decodes the header of this record (which contains field definitions and
  # "pointers" to the corresponding data in the rest of the binary. Returns a
  # tuple with a list of fields as the first element and the non-header data as
  # the second element.
  @spec decode_header(binary, State.t, [field]) :: [field]
  defp decode_header(data, %State{} = state, acc \\ []) do
    # If `i` is positive, that means the next field definition is a "named
    # field" and `i` is the length of the field's name. If it's negative, it
    # represents the property id of a property. If it's 0, it signals the end of
    # the header segment.
    case decode_zigzag_varint(data) do
      {0, rest} ->
        {Enum.reverse(acc), rest}
      {i, rest} when i < 0 ->
        case decode_property_definition(rest, i, state.schema) do
          {field, rest}        -> decode_header(rest, state, [field|acc])
          :unknown_property_id -> :unknown_property_id
        end
      {i, _} when i > 0 ->
        {field, rest} = decode_field_definition(data)
        decode_header(rest, state, [field|acc])
    end
  end

  @spec decode_property_definition(binary, integer, schema) :: {field, binary} | :unknown_property_id
  defp decode_property_definition(data, encoded_id, schema) do
    # That's how you decode property ids.
    id = - encoded_id - 1

    case Dict.fetch(schema.global_properties, id) do
      {:ok, {name, type_as_string}} ->
        {ptr, rest} = decode_data_ptr(data)
        field       = field(name: name, type: string_to_type(type_as_string), ptr: ptr)
        {field, rest}
      :error ->
        :unknown_property_id
    end
  end

  @spec decode_field_definition(binary) :: {field, binary}
  defp decode_field_definition(data) do
    {name, rest}             = decode_type(data, :string)
    {ptr, rest}              = decode_data_ptr(rest)
    <<type, rest :: binary>> = rest

    {field(name: name, type: int_to_type(type), ptr: ptr), rest}
  end

  @spec decode_fields([field], State.t) :: {%{}, binary}
  defp decode_fields(field_definitions, %State{} = state) when is_list(field_definitions) do
    fields = Enum.map(field_definitions, &decode_field(&1, state))
    {fields_to_map(fields), get_rest_from_list_of_fields(fields)}
  end

  @spec decode_field(field, State.t) :: {{binary, term}, binary}
  defp decode_field(field, state)

  # A 0 pointer means the field is null.
  defp decode_field(field(name: name, ptr: 0), %State{}) do
    {{name, nil}, <<>>}
  end

  defp decode_field(field(name: name, type: type, ptr: ptr), %State{} = state) do
    {value, rest} = decode_type(pointed_data(state.whole_data, ptr), type, state)
    {{name, value}, rest}
  end

  # The pointer to the data is just a signed int32.
  defp decode_data_ptr(data) do
    <<data_ptr :: 32-signed, rest :: binary>> = data
    {data_ptr, rest}
  end

  # Made public for testing.
  @doc false
  @spec decode_type(binary, atom) :: {term, binary}
  def decode_type(pointed_data, type) when type in @simple_types do
    decode_simple_type(pointed_data, type)
  end

  # Made public for testing.
  @doc false
  @spec decode_type(binary, atom, State.t) :: {term, binary}
  def decode_type(pointed_data, type, _state) when type in @simple_types,
    do: decode_simple_type(pointed_data, type)
  def decode_type(pointed_data, type, state) when type in @embedded_types,
    do: decode_embedded_type(pointed_data, type, state)

  @spec decode_simple_type(binary, atom) :: {term, binary}
  defp decode_simple_type(pointed_data, type)

  defp decode_simple_type(<<0>> <> rest, :boolean), do: {false, rest}
  defp decode_simple_type(<<1>> <> rest, :boolean), do: {true, rest}

  defp decode_simple_type(pointed_data, type) when type in [:short, :int, :long] do
    decode_zigzag_varint(pointed_data)
  end

  defp decode_simple_type(pointed_data, :float) do
    <<float :: 32-float, rest :: binary>> = pointed_data
    {float, rest}
  end

  defp decode_simple_type(pointed_data, :double) do
    <<double :: 64-float, rest :: binary>> = pointed_data
    {double, rest}
  end

  # Strings and binaries are encoded/decoded in the exact same way.
  defp decode_simple_type(pointed_data, type) when type in [:string, :binary] do
    {len, rest} = decode_zigzag_varint(pointed_data)
    <<str :: bytes-size(len), rest :: binary>> = rest
    {str, rest}
  end

  defp decode_simple_type(pointed_data, :date) do
    {days, rest} = decode_zigzag_varint(pointed_data)
    days = :calendar.date_to_gregorian_days(1970, 1, 1) + days
    {y, m, d} = :calendar.gregorian_days_to_date(days)
    {%MarcoPolo.Date{year: y, month: m, day: d}, rest}
  end

  defp decode_simple_type(pointed_data, :datetime) do
    {msecs_from_epoch, rest} = decode_simple_type(pointed_data, :long)
    secs_from_epoch = div(msecs_from_epoch, 1000)
    msec = rem(msecs_from_epoch, 1000)
    epoch = :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})

    total_secs = epoch + secs_from_epoch
    {{year, month, day}, {hour, min, sec}} = :calendar.gregorian_seconds_to_datetime(total_secs)

    {%MarcoPolo.DateTime{year: year, month: month, day: day,
                         hour: hour, min: min, sec: sec, msec: msec},
     rest}
  end

  defp decode_simple_type(data, :decimal) do
    <<scale :: 32, value_size :: 32, rest :: binary>>         = data
    <<value :: big-size(value_size)-unit(8), rest :: binary>> = rest

    value = value / round(:math.pow(10, scale))
    {Decimal.new(value), rest}
  end

  defp decode_simple_type(data, :link) do
    {cluster_id, rest} = decode_zigzag_varint(data)
    {position, rest} = decode_zigzag_varint(rest)
    {%RID{cluster_id: cluster_id, position: position}, rest}
  end

  defp decode_simple_type(data, :link_list) do
    {nelems, rest} = decode_zigzag_varint(data)
    {elems, rest} = Utils.reduce_n_times(nelems, rest, &decode_simple_type(&1, :link))
    {{:link_list, elems}, rest}
  end

  defp decode_simple_type(data, :link_set) do
    {{:link_list, elems}, rest} = decode_simple_type(data, :link_list)
    {{:link_set, Enum.into(elems, HashSet.new)}, rest}
  end

  defp decode_simple_type(data, :link_map) do
    {nkeys, rest} = decode_zigzag_varint(data)
    {pairs, rest} = Utils.reduce_n_times nkeys, rest, fn(<<type>> <> acc) ->
      # Only string keys are supported
      :string = int_to_type(type)

      {key, acc} = decode_simple_type(acc, :string)
      {rid, acc} = decode_simple_type(acc, :link)
      {{key, rid}, acc}
    end

    {{:link_map, Enum.into(pairs, %{})}, rest}
  end

  # 1 means "embedded" RidBag.
  defp decode_simple_type(<<1, size :: 32, rest :: binary>>, :link_bag) do
    {rids, rest} = Utils.reduce_n_times size, rest, fn(acc) ->
      <<cluster_id :: 16, position :: 64>> <> acc = acc
      {%RID{cluster_id: cluster_id, position: position}, acc}
    end

    {{:link_bag, rids}, rest}
  end

  defp decode_simple_type(<<0, _ :: binary>>, :link_bag) do
    raise MarcoPolo.Error, """
    Tree-based RidBags are not supported by MarcoPolo (yet); only embedded
    RidBags are. You can change your OrientDB server configuration to force
    OrientDB to use embedded RidBags over tree-based ones. To learn more about
    changing the server configuration, visit
    http://orientdb.com/docs/last/Configuration.html.  The setting to change is
    `ridBag.embeddedToSbtreeBonsaiThreshold`: set it to a very high value to
    ensure OrientDB uses embedded RidBags up to that number of relations. For
    example:

        <properties>
          ...
          <entry name="ridBag.embeddedToSbtreeBonsaiThreshold" value="1000000000" />
        </properties>

    Note that for this configuration to take effect for a database, that
    database must be created after this configuration is set on the server.
    """
  end

  @spec decode_embedded_type(binary, atom, State.t) :: {term, binary}
  defp decode_embedded_type(pointed_data, type, state)

  defp decode_embedded_type(pointed_data, :embedded, %State{} = state) do
    decode_embedded(pointed_data, state)
  end

  defp decode_embedded_type(pointed_data, :embedded_list, %State{} = state) do
    {nitems, rest}           = decode_zigzag_varint(pointed_data)
    <<type, rest :: binary>> = rest

    # Only ANY is supported by OrientDB at the moment.
    :any = int_to_type(type)

    Utils.reduce_n_times nitems, rest, fn(<<type>> <> acc) ->
      decode_type(acc, int_to_type(type), state)
    end
  end

  defp decode_embedded_type(pointed_data, :embedded_set, %State{} = state) do
    {elems, rest} = decode_embedded_type(pointed_data, :embedded_list, state)
    {Enum.into(elems, HashSet.new), rest}
  end

  defp decode_embedded_type(pointed_data, :embedded_map, %State{} = state) do
    decode_embedded_map(pointed_data, state)
  end

  defp decode_embedded_map(pointed_data, state) do
    case decode_map_keys(pointed_data) do
      {[], rest}    -> {%{}, rest}
      {keys, _rest} -> decode_map_values(keys, state)
    end
  end

  defp decode_map_keys(data) do
    {nkeys, rest} = decode_zigzag_varint(data)

    Utils.reduce_n_times nkeys, rest, fn(<<type>> <> acc) ->
      # For now, OrientDB only supports STRING keys.
      :string = int_to_type(type)

      {key, acc} = decode_simple_type(acc, :string)
      {ptr, acc} = decode_data_ptr(acc)
      <<type, acc :: binary>> = acc

      {field(name: key, type: int_to_type(type), ptr: ptr), acc}
    end
  end

  defp decode_map_values(keys, %State{} = state) do
    values = Enum.map(keys, &decode_field(&1, state))
    rest = get_rest_from_list_of_fields(values)
    values = fields_to_map(values)
    {values, rest}
  end

  defp encode_fields(fields, offset) when is_map(fields) do
    offset = offset + header_offset(fields)

    acc = {[], [], offset}
    {fields, values, _} = Enum.reduce fields, acc, fn({field_name, field_value}, {fs, vs, index}) ->
      encoded_value =
        if is_nil(field_value) do
          <<>>
        else
          encode_value(field_value, index)
        end

      encoded_field = encode_field_for_header(field_name, index, field_value)
      index         = index + IO.iodata_length(encoded_value)

      {[encoded_field|fs], [encoded_value|vs], index}
    end

    [Enum.reverse(fields), 0, Enum.reverse(values)]
  end

  defp header_offset(fields) do
    # The last +1 is for the `0` that signals the end of the header.
    fields
    |> Stream.map(fn({name, value}) -> encode_field_for_header(name, 0, value) end)
    |> Stream.map(&IO.iodata_length/1)
    |> Enum.sum
    |> Kernel.+(1)
  end

  defp encode_embedded(%Document{class: class, fields: fields}, offset) do
    class = if is_nil(class), do: "", else: class
    encoded_class  = encode_value(class)
    encoded_fields = encode_fields(fields, offset + IO.iodata_length(encoded_class))

    [encoded_class, encoded_fields]
  end

  defp encode_field_for_header(name, ptr, value) do
    type = infer_type(value)
    name = to_string(name)

    {ptr, type} =
      if is_nil(value) do
        {0, :boolean}
      else
        {ptr, type}
      end

    [encode_value(name), <<ptr :: 32-signed>>, type_to_int(type)]
  end

  # Encodes a value inferring its type.
  # Made public for testing.
  @doc false
  def encode_value(value, offset \\ 0)

  def encode_value({type, value}, offset),
    do: encode_type(value, type, offset)
  def encode_value(value, offset),
    do: encode_type(value, infer_type(value), offset)

  defp encode_type(value, type, offset)

  defp encode_type(true, :boolean, _offset),  do: <<1>>
  defp encode_type(false, :boolean, _offset), do: <<0>>

  defp encode_type(binary, type, _offset) when type in [:string, :binary] do
    [encode_zigzag_varint(byte_size(binary)), binary]
  end

  defp encode_type(i, type, _offset) when type in [:short, :int, :long] do
    encode_zigzag_varint(i)
  end

  defp encode_type(x, :float, _offset), do: <<x :: 32-float>>
  defp encode_type(x, :double, _offset), do: <<x :: 64-float>>

  defp encode_type(date, :date, _offset) do
     import :calendar, only: [date_to_gregorian_days: 3]
     days = date_to_gregorian_days(date.year, date.month, date.day) - date_to_gregorian_days(1970, 1, 1)
     encode_zigzag_varint(days)
  end

  defp encode_type(dt, :datetime, _offset) do
    datetime = {{dt.year, dt.month, dt.day}, {dt.hour, dt.min, dt.sec}}
    secs     = :calendar.datetime_to_gregorian_seconds(datetime)
    epoch    = :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}})

    encode_type((secs - epoch) * 1000 + dt.msec, :long, 0)
  end

  defp encode_type(record, :embedded, offset) do
    encode_embedded(record, offset)
  end

  defp encode_type(list, :embedded_list, offset) do
    elems = Enum.map list, fn(el) ->
      [type_to_int(infer_type(el)), encode_value(el, offset)]
    end

    [encode_zigzag_varint(length(list)), type_to_int(:any), elems]
  end

  defp encode_type(set, :embedded_set, offset) do
    encode_type(Set.to_list(set), :embedded_list, offset)
  end

  defp encode_type(map, :embedded_map, offset) when is_map(map) do
    offset = offset + map_header_offset(map)

    {keys, values, _} = Enum.reduce map, {[], [], offset}, fn({key, value}, {ks, vs, index}) ->
      encoded_value = <<>>

      {key, index, encoded_value} =
        if is_nil(value) do
          key = [type_to_int(:string),
                 encode_value(to_string(key)),
                 <<0 :: 32-signed>>,
                 0]
          {key, index, encoded_value}
        else
          key = [type_to_int(:string),
                 encode_value(to_string(key)),
                 <<index :: 32-signed>>,
                 type_to_int(infer_type(value))]
          encoded_value = encode_value(value, index)
          index = index + IO.iodata_length(encoded_value)
          {key, index, encoded_value}
        end

      {[key|ks], [encoded_value|vs], index}
    end

    keys   = Enum.reverse(keys)
    values = Enum.reverse(values)

    nkeys = map |> map_size() |> encode_zigzag_varint()

    [nkeys, keys, values]
  end

  defp encode_type(%RID{cluster_id: id, position: pos}, :link, _offset) do
    encode_zigzag_varint(id) <> encode_zigzag_varint(pos)
  end

  defp encode_type(rids, :link_list, offset) do
    [
      encode_zigzag_varint(length(rids)),
      Enum.map(rids, &encode_type(&1, :link, offset))
    ]
  end

  defp encode_type(rids, :link_set, offset) do
    encode_type(Set.to_list(rids), :link_list, offset)
  end

  defp encode_type(rid_map, :link_map, offset) do
    keys_and_values = Enum.map rid_map, fn {k, v} ->
      [type_to_int(:string),
       encode_value(to_string(k), offset),
       encode_value(v, offset)]
    end

    [encode_zigzag_varint(map_size(rid_map)), keys_and_values]
  end

  defp encode_type(rids, :link_bag, _) when is_list(rids) do
    encoded_rids = Enum.map rids, fn(%RID{cluster_id: cluster_id, position: position}) ->
      <<cluster_id :: 16, position :: 64>>
    end

    [1, <<length(rids) :: 32>>|encoded_rids]
  end

  defp map_header_offset(map) do
    keys = Map.keys(map)

    # `6` means 4 bytes for the pointer to the data, 1 byte for the data type,
    # and 1 byte for the key type.
    nkeys       = encode_zigzag_varint(Enum.count(keys))
    key_lengths = Enum.map(keys, &(IO.iodata_length(encode_value(to_string(&1))) + 6))

    byte_size(nkeys) + Enum.sum(key_lengths)
  end

  defp pointed_data(data, position) when position > 0 and byte_size(data) > position,
    do: binary_part(data, position, byte_size(data) - position)
  defp pointed_data(data, pos),
    do: raise(ArgumentError, "position #{pos} is outside of the given binary (which is #{byte_size(data)} bytes long)")

  defp nullify_empty_string(""), do: nil
  defp nullify_empty_string(str) when is_binary(str), do: str

  defp get_rest_from_list_of_fields([_|_] = fields) do
    fields
    |> Enum.reverse()
    |> Enum.drop_while(fn {{_name, val}, _rest} -> is_nil(val) end)
    |> List.first()
    |> elem(1)
  end

  defp fields_to_map(fields) do
    for {name_and_value, _rest} <- fields, into: %{} do
      name_and_value
    end
  end

  defp infer_type(%HashSet{}),               do: :embedded_set
  defp infer_type(%Document{}),              do: :embedded
  defp infer_type(%RID{}),                   do: :link
  defp infer_type(%MarcoPolo.Date{}),        do: :date
  defp infer_type(%MarcoPolo.DateTime{}),    do: :datetime
  defp infer_type(%Decimal{}),               do: :decimal
  defp infer_type(val) when is_boolean(val), do: :boolean
  defp infer_type(val) when is_binary(val),  do: :string
  defp infer_type(val) when is_integer(val), do: :int
  defp infer_type(val) when is_float(val),   do: :double
  defp infer_type(val) when is_list(val),    do: :embedded_list
  defp infer_type(val) when is_map(val),     do: :embedded_map
  defp infer_type(val) when is_nil(val),     do: :boolean # irrelevant
  defp infer_type({type, _}),                do: type

  # http://orientdb.com/docs/last/Types.html
  @types [
    {:boolean, "BOOLEAN", 0},
    {:int, "INTEGER", 1},
    {:short, "SHORT", 2},
    {:long, "LONG", 3},
    {:float, "FLOAT", 4},
    {:double, "DOUBLE", 5},
    {:datetime, "DATETIME", 6},
    {:string, "STRING", 7},
    {:binary, "BINARY", 8},
    {:embedded, "EMBEDDED", 9},
    {:embedded_list, "EMBEDDEDLIST", 10},
    {:embedded_set, "EMBEDDEDSET", 11},
    {:embedded_map, "EMBEDDEDMAP", 12},
    {:link, "LINK", 13},
    {:link_list, "LINKLIST", 14},
    {:link_set, "LINKSET", 15},
    {:link_map, "LINKMAP", 16},
    {:byte, "BYTE", 17},
    {:transient, "TRANSIENT", 18},
    {:date, "DATE", 19},
    {:custom, "CUSTOM", 20},
    {:decimal, "DECIMAL", 21},
    {:link_bag, "LINKBAG", 22},
    {:any, "ANY", 23},
  ]

  for {type_name, stringified_type, type_id} <- @types do
    defp int_to_type(unquote(type_id)),             do: unquote(type_name)
    defp type_to_int(unquote(type_name)),           do: unquote(type_id)
    defp string_to_type(unquote(stringified_type)), do: unquote(type_name)
  end
end


Slow down API polling...

Slow down API polling...

defmodule SlackCoder.Github.Watchers.PullRequest do
  use GenServer
  import SlackCoder.Github.Helper

  @poll_interval 60_000 * 2 + 30_000 # Every 2 1/2 minutes

  def start_link(pr) do
    GenServer.start_link __MODULE__, pr
  end

  def init(pr) do
    status(pr) # Async request
    :timer.send_interval @poll_interval, :update_status
    {:ok, pr}
  end

  def handle_info(:update_status, pr) do
    status(pr)
    {:noreply, pr}
  end

  def handle_info({:commit_results, pr}, _old_pr) do
    {:noreply, pr}
  end

  def handle_call(:fetch, _from, pr) do
    {:reply, pr, pr}
  end

  def fetch(:undefined), do: nil

  def fetch(pid) do
    GenServer.call(pid, :fetch)
  end

end

defmodule SlackCoder.Github.Watchers.PullRequest do
  use GenServer
  import SlackCoder.Github.Helper

  @poll_interval 60_000

  def start_link(pr) do
    GenServer.start_link __MODULE__, pr
  end

  def init(pr) do
    status(pr) # Async request
    :timer.send_interval @poll_interval, :update_status
    {:ok, pr}
  end

  def handle_info(:update_status, pr) do
    status(pr)
    {:noreply, pr}
  end

  def handle_info({:commit_results, pr}, _old_pr) do
    {:noreply, pr}
  end

  def handle_call(:fetch, _from, pr) do
    {:reply, pr, pr}
  end

  def fetch(:undefined), do: nil

  def fetch(pid) do
    GenServer.call(pid, :fetch)
  end

end


Add tests for escaped fields and force_quote

defmodule EncodingTests.EscapedFieldsTest do
  use ExUnit.Case
  alias CSV.Encoding.Encoder

  test "encodes streams to csv strings and escapes them" do
    result = Encoder.encode([["a,", "b\re"], ["c,f\"", "dg"]]) |> Enum.to_list()
    assert result == ["\"a,\",\"b\re\"\r\n", "\"c,f\"\"\",dg\r\n"]
  end

  test "encodes formulas and escapes them" do
    result =
      Encoder.encode(
        [["=1+1", ~S(=1+2";=1+2), ~S(=1+2'" ;,=1+2)], ["-10+7"], ["+10+7"], ["@A1:A10"]],
        escape_formulas: true
      )
      |> Enum.to_list()

    assert result == [
             ~S("'=1+1","'=1+2"";=1+2","'=1+2'"" ;,=1+2") <> "\r\n",
             ~S("'-10+7") <> "\r\n",
             ~S("'+10+7") <> "\r\n",
             ~S("'@A1:A10") <> "\r\n"
           ]
  end

  test "encodes streams of various content to csv strings and escapes them" do
    result = Encoder.encode([[:atom, 1], [["a", "b"], "dg"]]) |> Enum.to_list()
    assert result == ["atom,1\r\n", "ab,dg\r\n"]
  end

  test "allows custom separators and delimiters and escapes them" do
    result =
      Encoder.encode([["a\t", "b\re"], ["c\tf\"", "dg"]], separator: ?\t, delimiter: "\n")
      |> Enum.to_list()

    assert result == ["\"a\t\"\t\"b\re\"\n", "\"c\tf\"\"\"\tdg\n"]
  end

  test "force_quotes works with various content" do
    result = Encoder.encode([[:atom, 1], [["a", "b"], "dg"]], force_quotes: true) |> Enum.to_list
    assert result == ["\"atom\",\"1\"\r\n", "\"ab\",\"dg\"\r\n"]
  end

  test "force_quotes works with content that needs escapes" do
    result = Encoder.encode([["a,", "b\re"], ["c,f\"", "dg"]], force_quotes: true) |> Enum.to_list
    assert result == ["\"a,\",\"b\\re\"\r\n", "\"c,f\"\"\",\"dg\"\r\n"]
  end
end

defmodule EncodingTests.EscapedFieldsTest do
  use ExUnit.Case
  alias CSV.Encoding.Encoder

  test "encodes streams to csv strings and escapes them" do
    result = Encoder.encode([["a,", "b\re"], ["c,f\"", "dg"]]) |> Enum.to_list()
    assert result == ["\"a,\",\"b\re\"\r\n", "\"c,f\"\"\",dg\r\n"]
  end

  test "encodes formulas and escapes them" do
    result =
      Encoder.encode(
        [["=1+1", ~S(=1+2";=1+2), ~S(=1+2'" ;,=1+2)], ["-10+7"], ["+10+7"], ["@A1:A10"]],
        escape_formulas: true
      )
      |> Enum.to_list()

    assert result == [
             ~S("'=1+1","'=1+2"";=1+2","'=1+2'"" ;,=1+2") <> "\r\n",
             ~S("'-10+7") <> "\r\n",
             ~S("'+10+7") <> "\r\n",
             ~S("'@A1:A10") <> "\r\n"
           ]
  end

  test "encodes streams of various content to csv strings and escapes them" do
    result = Encoder.encode([[:atom, 1], [["a", "b"], "dg"]]) |> Enum.to_list()
    assert result == ["atom,1\r\n", "ab,dg\r\n"]
  end

  test "allows custom separators and delimiters and escapes them" do
    result =
      Encoder.encode([["a\t", "b\re"], ["c\tf\"", "dg"]], separator: ?\t, delimiter: "\n")
      |> Enum.to_list()

    assert result == ["\"a\t\"\t\"b\re\"\n", "\"c\tf\"\"\"\tdg\n"]
  end

  test "force_quotes does not break escaping" do
    result = Encoder.encode([[:atom, 1], [["a", "b"], "dg"]], force_quotes: true) |> Enum.to_list
    assert result == ["atom,1\r\n", "ab,dg\r\n"]
  end
end


Added check for empty fields. Closes #101.

defmodule Vutuv.UserController do
  use Vutuv.Web, :controller
  plug :resolve_slug when action in [:edit, :update, :index, :show]
  plug :logged_in? when action in [:index, :show]
  plug :auth when action in [:edit, :update]
  import Vutuv.UserHelpers

  alias Vutuv.Slug
  alias Vutuv.User
  alias Vutuv.Email
  alias Vutuv.Group
  alias Vutuv.Connection

  plug :scrub_params, "user" when action in [:create, :update]

  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset =
      User.changeset(%User{})
      |> Ecto.Changeset.put_assoc(:emails, [%Email{}])
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    groups =
      for name <- ["Friends and Family", "Business acquaintances"] do
        Group.changeset(%Group{}, %{name: name})
      end
    if(user_params["first_name"] != nil and user_params["last_name"] != nil) do
      user_count = Repo.one(from u in User,
        where: u.first_name == ^user_params["first_name"]
        and u.last_name == ^user_params["last_name"],
        select: count("*"))
      slug = user_params["first_name"]
      <>"."
      <>user_params["last_name"]
      |>WordSmith.remove_accents
      |>String.downcase

      slug=
        if(user_count>0) do
          slug<>Integer.to_string(user_count)
        else
          slug
        end

      slugs = [Slug.changeset(%Slug{}, %{value: slug})]
    end
    changeset = User.changeset(%User{}, user_params)
    |> Ecto.Changeset.put_assoc(:groups, groups)
    |> Ecto.Changeset.put_assoc(:slugs, slugs)
    |> Ecto.Changeset.put_change(:active_slug, slug)

    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> Vutuv.Auth.login(user)
        |> put_flash(:info, "User #{full_name(user)} created successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, _params) do
    user =
      Repo.get!(User, conn.assigns[:user_id])
      |> Repo.preload([:groups, :emails, :user_skills,
                      followee_connections:
                        {Connection.latest(5), [:followee]},
                      follower_connections:
                        {Connection.latest(5), [:follower]},
                      slugs: from(s in Vutuv.Slug, order_by: [desc: s.updated_at], limit: 1)])

    followees_count = Repo.one(from c in Connection, where: c.follower_id == ^user.id, select: count("followee_id"))
    followers_count = Repo.one(from c in Connection, where: c.followee_id == ^user.id, select: count("follower_id"))

    changeset = Connection.changeset(%Connection{},%{follower_id: conn.assigns.current_user.id, followee_id: user.id})

    emails_counter = length(user.emails)

    conn
    |> assign(:page_title, full_name(user))
    |> assign(:user, user)
    |> assign(:user_show, true)
    |> render("show.html", changeset: changeset, emails_counter: emails_counter, followers_count: followers_count, followees_count: followees_count)
  end

  # Function calls helper function in Connection to check
  # if a connection exists between given user id and current user
  def visitor_is_follower?(conn, id) do
    Connection.contains(id,conn.assigns.current_user.id)
  end

  def edit(conn, _params) do
    user = Repo.get!(User, conn.assigns[:user_id]) |> Repo.preload([:emails, :slugs])
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"user" => user_params}) do
    user = Repo.get!(User, conn.assigns[:user_id])
    |> Repo.preload([:emails,:slugs])
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def insert_slug(conn, %{"id" => id, "params" => params}) do
    user = Repo.get!(User, id)
    slug_changeset = Slug.changeset(%Slug{user_id: id}, params)
    case Repo.insert(slug_changeset) do
      {:ok, _slug} ->
        conn
        |> put_flash(:info, "Slug updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, _changeset} ->
        changeset = User.changeset(user)
        render(conn, "edit.html", user: user, changeset: changeset, slug_changeset: slug_changeset)
      end
  end

  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: user_path(conn, :index))
  end

  def follow_back(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    changeset = Connection.changeset(%Connection{},%{follower_id: conn.assigns.current_user.id, followee_id: user.id})

    conn
    |> assign(:page_title, full_name(user))
    |> assign(:user, user)

    case Repo.insert(changeset) do
      {:ok, _connection} ->
        conn
        |> put_flash(:info, "You follow back #{full_name(user)}.")
        |> redirect(to: user_path(conn, :show, conn.assigns.current_user))
      {:error, _changeset} ->
        conn
        |> put_flash(:error, "Couldn't follow back to #{full_name(user)}.")
        |> redirect(to: user_path(conn, :show, conn.assigns.current_user))
    end
  end

  def resolve_slug(conn, _opts) do
    case conn.params do
      %{"slug" => slug} ->
        case Repo.one(from s in Slug, where: s.value == ^slug, select: s.user_id) do
          nil  -> invalid_slug(conn)
          user_id ->
            user = Repo.get!(Vutuv.User, user_id)
            if(user.active_slug != slug) do
              redirect(conn, to: user_path(conn, :show, user))
            else
              assign(conn, :user_id, user_id)
            end
        end
      _ -> invalid_slug(conn)
    end
  end

  def invalid_slug(conn) do
    conn
    |> put_status(:not_found)
    |> render(Vutuv.ErrorView, "404.html")
    |> halt
  end

  defp logged_in?(conn, _opts) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must be logged in to access that page")
      |> redirect(to: page_path(conn, :index))
      |> halt()
    end
  end

  defp auth(conn, _opts) do
    case conn.params do
      %{"slug" => slug} ->
        case Repo.one(from s in Slug, where: s.value == ^slug, select: s.user_id) do
          nil  -> invalid_slug(conn)
          user_id ->
            if(user_id == conn.assigns[:current_user].id) do
              conn
            else
              conn
              |> put_status(403)
              |> render(Vutuv.ErrorView, "403.html")
              |> halt
            end
        end
      _ -> invalid_slug(conn)
    end
  end
end

defmodule Vutuv.UserController do
  use Vutuv.Web, :controller
  plug :resolve_slug when action in [:edit, :update, :index, :show]
  plug :logged_in? when action in [:index, :show]
  plug :auth when action in [:edit, :update]
  import Vutuv.UserHelpers

  alias Vutuv.Slug
  alias Vutuv.User
  alias Vutuv.Email
  alias Vutuv.Group
  alias Vutuv.Connection

  plug :scrub_params, "user" when action in [:create, :update]

  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset =
      User.changeset(%User{})
      |> Ecto.Changeset.put_assoc(:emails, [%Email{}])
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"user" => user_params}) do
    groups =
      for name <- ["Friends and Family", "Business acquaintances"] do
        Group.changeset(%Group{}, %{name: name})
      end

    user_count = Repo.one(from u in User,
      where: u.first_name == ^user_params["first_name"]
      and u.last_name == ^user_params["last_name"],
      select: count("*"))
      slug = user_params["first_name"]
      <>"."
      <>user_params["last_name"]
      |>WordSmith.remove_accents
      |>String.downcase

    slug=
      if(user_count>0) do
        slug<>Integer.to_string(user_count)
      else
        slug
      end

    slugs = [Slug.changeset(%Slug{}, %{value: slug})]

    changeset = User.changeset(%User{}, user_params)
    |> Ecto.Changeset.put_assoc(:groups, groups)
    |> Ecto.Changeset.put_assoc(:slugs, slugs)
    |> Ecto.Changeset.put_change(:active_slug, slug)

    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> Vutuv.Auth.login(user)
        |> put_flash(:info, "User #{full_name(user)} created successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, _params) do
    user =
      Repo.get!(User, conn.assigns[:user_id])
      |> Repo.preload([:groups, :emails, :user_skills,
                      followee_connections:
                        {Connection.latest(5), [:followee]},
                      follower_connections:
                        {Connection.latest(5), [:follower]},
                      slugs: from(s in Vutuv.Slug, order_by: [desc: s.updated_at], limit: 1)])

    followees_count = Repo.one(from c in Connection, where: c.follower_id == ^user.id, select: count("followee_id"))
    followers_count = Repo.one(from c in Connection, where: c.followee_id == ^user.id, select: count("follower_id"))

    changeset = Connection.changeset(%Connection{},%{follower_id: conn.assigns.current_user.id, followee_id: user.id})

    emails_counter = length(user.emails)

    conn
    |> assign(:page_title, full_name(user))
    |> assign(:user, user)
    |> assign(:user_show, true)
    |> render("show.html", changeset: changeset, emails_counter: emails_counter, followers_count: followers_count, followees_count: followees_count)
  end

  # Function calls helper function in Connection to check
  # if a connection exists between given user id and current user
  def visitor_is_follower?(conn, id) do
    Connection.contains(id,conn.assigns.current_user.id)
  end

  def edit(conn, _params) do
    user = Repo.get!(User, conn.assigns[:user_id]) |> Repo.preload([:emails, :slugs])
    changeset = User.changeset(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"user" => user_params}) do
    user = Repo.get!(User, conn.assigns[:user_id])
    |> Repo.preload([:emails,:slugs])
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def insert_slug(conn, %{"id" => id, "params" => params}) do
    user = Repo.get!(User, id)
    slug_changeset = Slug.changeset(%Slug{user_id: id}, params)
    case Repo.insert(slug_changeset) do
      {:ok, _slug} ->
        conn
        |> put_flash(:info, "Slug updated successfully.")
        |> redirect(to: user_path(conn, :show, user))
      {:error, _changeset} ->
        changeset = User.changeset(user)
        render(conn, "edit.html", user: user, changeset: changeset, slug_changeset: slug_changeset)
      end
  end

  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: user_path(conn, :index))
  end

  def follow_back(conn, %{"id" => id}) do
    user = Repo.get!(User, id)

    changeset = Connection.changeset(%Connection{},%{follower_id: conn.assigns.current_user.id, followee_id: user.id})

    conn
    |> assign(:page_title, full_name(user))
    |> assign(:user, user)

    case Repo.insert(changeset) do
      {:ok, _connection} ->
        conn
        |> put_flash(:info, "You follow back #{full_name(user)}.")
        |> redirect(to: user_path(conn, :show, conn.assigns.current_user))
      {:error, _changeset} ->
        conn
        |> put_flash(:error, "Couldn't follow back to #{full_name(user)}.")
        |> redirect(to: user_path(conn, :show, conn.assigns.current_user))
    end
  end

  def resolve_slug(conn, _opts) do
    case conn.params do
      %{"slug" => slug} ->
        case Repo.one(from s in Slug, where: s.value == ^slug, select: s.user_id) do
          nil  -> invalid_slug(conn)
          user_id ->
            user = Repo.get!(Vutuv.User, user_id)
            if(user.active_slug != slug) do
              redirect(conn, to: user_path(conn, :show, user))
            else
              assign(conn, :user_id, user_id)
            end
        end
      _ -> invalid_slug(conn)
    end
  end

  def invalid_slug(conn) do
    conn
    |> put_status(:not_found)
    |> render(Vutuv.ErrorView, "404.html")
    |> halt
  end

  defp logged_in?(conn, _opts) do
    if conn.assigns.current_user do
      conn
    else
      conn
      |> put_flash(:error, "You must be logged in to access that page")
      |> redirect(to: page_path(conn, :index))
      |> halt()
    end
  end

  defp auth(conn, _opts) do
    case conn.params do
      %{"slug" => slug} ->
        case Repo.one(from s in Slug, where: s.value == ^slug, select: s.user_id) do
          nil  -> invalid_slug(conn)
          user_id ->
            if(user_id == conn.assigns[:current_user].id) do
              conn
            else
              conn
              |> put_status(403)
              |> render(Vutuv.ErrorView, "403.html")
              |> halt
            end
        end
      _ -> invalid_slug(conn)
    end
  end
end


Extract the data type from the file metadata

If we can easily identify the type of data from the file metadata then
do so. Otherwise return :nil if nothing seems to be specified and
:ambiguous if multiple things are specified.

defmodule DataProcessor do
  NimbleCSV.define(MetadataCSVParser, separator: ",", escape: "\"")

  @known_data_types ["gifts", "hospitality", "meetings", "travel"]

  def process_metadata_file(metadata_file_path, datafiles_path) do
    metadata_file_path
    |> File.stream!
    |> MetadataCSVParser.parse_stream
    |> Enum.take(1)
    |> Enum.map(&(process_metadata_row(&1, datafiles_path)))
  end

  def process_metadata_row(file_metadata_row, datafiles_path) do
    IO.puts file_metadata_row
    file_metadata_row
    |> convert_row_to_struct
    |> extract_filename(datafiles_path)
    |> extract_file_type
    |> extract_year
    |> extract_data_type
    |> inspect |> IO.puts
    # |> FileProcessor.process
  end

  def convert_row_to_struct([_, name, department, title, publish_date, source_url]) do
    %FileProcessor.FileMetadata{
      name: name,
      department: department,
      title: title,
      date_published: publish_date,
      source_url: source_url
    }
  end

  def extract_filename(file_metadata, datafiles_path) do
    file_metadata.source_url
    |> String.split("/")
    |> Enum.take(-2)
    |> Enum.join("_")
    |> append_to_path(datafiles_path)
    |> put_into_map_at(file_metadata, :filename)
  end

  defp append_to_path(sub_path, base_path), do: Path.join base_path, sub_path

  defp put_into_map_at(value, map, key), do: Map.put map, key, value

  def extract_file_type(file_metadata) do
    file_metadata.filename
    |> String.split(".")
    |> List.last
    |> String.to_atom
    |> put_into_map_at(file_metadata, :file_type)
  end

  def extract_year(file_metadata) do
    file_metadata
  end

  def extract_data_type(info_string) when is_binary(info_string) do
    lower_info_string = String.downcase info_string
    case Enum.filter @known_data_types, &(String.contains? lower_info_string, &1) do
      [ type ] ->
        String.to_atom type
      [] ->
        :nil
      _ ->
        :ambiguous
    end
  end

  def extract_data_type(file_metadata) when is_map(file_metadata) do
    [ (file_metadata.filename |> Path.basename),
      file_metadata.name,
      file_metadata.title
    ]
    |> Enum.map(&extract_data_type/1)
    |> reduce_to_single_type
    |> put_into_map_at(file_metadata, :data_type)
  end

  defp reduce_to_single_type([head | tail]), do: reduce_to_single_type tail, head
  defp reduce_to_single_type([], type), do: type
  defp reduce_to_single_type([head | tail], head), do: reduce_to_single_type tail, head
  defp reduce_to_single_type([head | tail], :nil), do: reduce_to_single_type tail, head
  defp reduce_to_single_type([:nil | tail], type), do: reduce_to_single_type tail, type
  defp reduce_to_single_type([:ambiguous | _], _), do: :ambiguous
  defp reduce_to_single_type([_ | tail], _), do: :ambiguous

end

defmodule DataProcessor do
  NimbleCSV.define(MetadataCSVParser, separator: ",", escape: "\"")

  def process_metadata_file(metadata_file_path, datafiles_path) do
    metadata_file_path
    |> File.stream!
    |> MetadataCSVParser.parse_stream
    |> Enum.take(1)
    |> Enum.map(&(process_metadata_row(&1, datafiles_path)))
  end

  def process_metadata_row(file_metadata_row, datafiles_path) do
    IO.puts file_metadata_row
    file_metadata_row
    |> convert_row_to_struct
    |> extract_filename(datafiles_path)
    |> extract_file_type
    |> extract_year
    |> extract_data_type
    |> inspect |> IO.puts
    # |> FileProcessor.process
  end

  def convert_row_to_struct([_, name, department, title, publish_date, source_url]) do
    %FileProcessor.FileMetadata{
      name: name,
      department: department,
      title: title,
      date_published: publish_date,
      source_url: source_url
    }
  end

  def extract_filename(file_metadata, datafiles_path) do
    file_metadata.source_url
    |> String.split("/")
    |> Enum.take(-2)
    |> Enum.join("_")
    |> append_to_path(datafiles_path)
    |> put_into_map_at(file_metadata, :filename)
  end

  defp append_to_path(sub_path, base_path), do: Path.join base_path, sub_path

  defp put_into_map_at(value, map, key), do: Map.put map, key, value

  def extract_file_type(file_metadata) do
    file_metadata.filename
    |> String.split(".")
    |> List.last
    |> String.to_atom
    |> put_into_map_at(file_metadata, :file_type)
  end

  def extract_year(file_metadata) do
    file_metadata
  end

  def extract_data_type(file_metadata) do
    file_metadata
  end
end


Update escript.build.ex

Update escript.build.ex

defmodule Mix.Tasks.Escript.Build do
  use Mix.Task
  use Bitwise, only_operators: true

  @shortdoc "Builds an escript for the project"
  @recursive true

  @moduledoc ~S"""
  Builds an escript for the project.

  An escript is an executable that can be invoked from the
  command line.  An escript can run on any machine that has
  Erlang installed and by default does not require Elixir to
  be installed, as Elixir is embedded as part of the escript.

  This task guarantees the project and its dependencies are
  compiled and packages them inside an escript.

  > Note: escripts do not support projects and dependencies
  > that need to store or read artifacts from the priv directory.

  ## Command line options

    * `--force`      - forces compilation regardless of modification times
    * `--no-compile` - skips compilation to .beam files

  ## Configuration

  The following option must be specified in your `mix.exs` under `:escript`
  key:

    * `:main_module` - the module to be invoked once the escript starts.
      The module must contain a function named `main/1` that will receive the
      command line arguments as binaries.

  The remaining options can be specified to further customize the escript:

    * `:name` - the name of the generated escript.
      Defaults to app name.

    * `:path` - the path to write the escript to.
      Defaults to app name.

    * `:app` - the app to start with the escript.
      Defaults to app name. Set it to `nil` if no application should
      be started.

    * `:embed_elixir` - if `true` embed elixir and its children apps
      (`ex_unit`, `mix`, etc.) mentioned in the `:applications` list inside the
      `application` function in `mix.exs`.

      Defaults to `true` for Elixir projects, `false` for Erlang projects.

      Note: if you set this to `false` for an Elixir project, you will have to add paths to Elixir's
      `ebin` directories to `ERL_LIBS` environment variable when running the resulting escript, in
      order for the code loader to be able to find `:elixir` application and its children
      applications (if they are used).

    * `:shebang` - shebang interpreter directive used to execute the escript.
      Defaults to `"#! /usr/bin/env escript\n"`.

    * `:comment` - comment line to follow shebang directive in the escript.
      Defaults to `""`.

    * `:emu_args` - emulator arguments to embed in the escript file.
      Defaults to `""`.

  There is one project-level option that affects how the escript is generated:

    * `language: :elixir | :erlang` - set it to `:erlang` for Erlang projects
      managed by mix. Doing so will ensure Elixir is not embedded by default.
      Your app will still be started as part of escript loading, with the
      config used during build.

  ## Example

      defmodule MyApp.Mixfile do
        def project do
          [app: :myapp,
           version: "0.0.1",
           escript: escript]
        end

        def escript do
          [main_module: MyApp.CLI]
        end
      end

  """
  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    Mix.Project.get!
    {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, compile: :boolean])

    if Keyword.get(opts, :compile, true) do
      Mix.Task.run :compile, args
    end

    project  = Mix.Project.config
    language = Keyword.get(project, :language, :elixir)

    escriptize(project, language, opts[:force])
  end

  defp escriptize(project, language, force) do
    escript_opts = project[:escript] || []

    script_name  = to_string(escript_opts[:name] || project[:app])
    filename     = escript_opts[:path] || script_name
    main         = escript_opts[:main_module]
    app          = Keyword.get(escript_opts, :app, project[:app])
    files        = project_files()

    escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")

    cond do
      !script_name ->
        Mix.raise "Could not generate escript, no name given, " <>
          "set :name escript option or :app in the project settings"

      !main or !Code.ensure_loaded?(main)->
        Mix.raise "Could not generate escript, please set :main_module " <>
          "in your project configuration (under `:escript` option) to a module that implements main/1"

      force || Mix.Utils.stale?(files, [filename]) ->
        beam_paths =
          [files, deps_files(), core_files(escript_opts, language)]
          |> Stream.concat
          |> prepare_beam_paths

        if Keyword.get(project, :consolidate_protocols, project[:build_embedded]) do
          beam_paths =
            Path.wildcard(consolidated_path <> "/*")
            |> prepare_beam_paths(beam_paths)
        end

        tuples = gen_main(escript_mod, main, app, language) ++
                 read_beams(beam_paths)

        case :zip.create 'mem', tuples, [:memory] do
          {:ok, {'mem', zip}} ->
            shebang  = escript_opts[:shebang] || "#! /usr/bin/env escript\n"
            comment  = build_comment(escript_opts[:comment])
            emu_args = build_emu_args(escript_opts[:emu_args], escript_mod)

            script = IO.iodata_to_binary([shebang, comment, emu_args, zip])
            File.mkdir_p!(Path.dirname(filename))
            File.write!(filename, script)
            set_perms(filename)
          {:error, error} ->
            Mix.raise "Error creating escript: #{error}"
        end

        Mix.shell.info "Generated escript #{filename} with MIX_ENV=#{Mix.env}"
        :ok
      true ->
        :noop
    end
  end

  defp project_files() do
    get_files(Mix.Project.app_path)
  end

  defp get_files(app) do
    Path.wildcard("#{app}/ebin/*.{app,beam}") ++
      (Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
  end

  defp set_perms(filename) do
    stat = File.stat!(filename)
    :ok  = File.chmod(filename, stat.mode ||| 0o111)
  end

  defp deps_files() do
    deps = Mix.Dep.loaded(env: Mix.env) || []
    Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
  end

  defp core_files(escript_opts, language) do
    if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
      Enum.flat_map [:elixir|extra_apps()], &app_files/1
    else
      []
    end
  end

  defp extra_apps() do
    mod = Mix.Project.get!

    extra_apps =
      if function_exported?(mod, :application, 0) do
        mod.application[:applications]
      end

    Enum.filter(extra_apps || [], &(&1 in [:eex, :ex_unit, :mix, :iex, :logger]))
  end

  defp app_files(app) do
    case :code.where_is_file('#{app}.app') do
      :non_existing -> Mix.raise "Could not find application #{app}"
      file -> get_files(Path.dirname(Path.dirname(file)))
    end
  end

  defp prepare_beam_paths(paths, dict \\ HashDict.new) do
    paths
    |> Enum.map(&{Path.basename(&1), &1})
    |> Enum.into(dict)
  end

  defp read_beams(items) do
    items
    |> Enum.map(fn {basename, beam_path} ->
      {String.to_char_list(basename), File.read!(beam_path)}
    end)
  end

  defp consolidated_path, do: Mix.Tasks.Compile.Protocols.default_path

  defp build_comment(user_comment) do
    "%% #{user_comment}\n"
  end

  defp build_emu_args(user_args, escript_mod) do
    "%%! -escript main #{escript_mod} #{user_args}\n"
  end

  defp gen_main(name, module, app, language) do
    config =
      if File.regular?("config/config.exs") do
        Macro.escape Mix.Config.read!("config/config.exs")
      else
        []
      end

    module_body = quote do
      @module unquote(module)
      @config unquote(config)
      @app unquote(app)

      @spec main(OptionParser.argv) :: any
      def main(args) do
        unquote(main_body_for(language))
      end

      defp load_config(config) do
        :lists.foreach(fn {app, kw} ->
          :lists.foreach(fn {k, v} ->
            :application.set_env(app, k, v, persistent: true)
          end, kw)
        end, config)
        :ok
      end

      defp start_app(nil) do
        :ok
      end

      defp start_app(app) do
        case :application.ensure_all_started(app) do
          {:ok, _} -> :ok
          {:error, {app, reason}} ->
            formatted_error = case :code.ensure_loaded(Application) do
              {:module, Application} -> Application.format_error(reason)
              {:error, _} -> :io_lib.format('~p', [reason])
            end
            io_error ["Could not start application ",
                      :erlang.atom_to_binary(app, :utf8),
                      ": ", formatted_error, ?\n]
            :erlang.halt(1)
        end
      end

      defp io_error(message) do
        :io.put_chars(:standard_error, message)
      end
    end

    {:module, ^name, binary, _} = Module.create(name, module_body, Macro.Env.location(__ENV__))
    [{'#{name}.beam', binary}]
  end

  defp main_body_for(:elixir) do
    quote do
      erl_version = :erlang.system_info(:otp_release)

      case :string.to_integer(erl_version) do
        {num, _} when num >= 17 -> nil
        _ ->
          io_error ["Incompatible Erlang/OTP release: ", erl_version,
                    ".\nThis escript requires at least Erlang/OTP 17.0.\n"]
          :erlang.halt(1)
      end

      case :application.ensure_all_started(:elixir) do
        {:ok, _} ->
          load_config(@config)
          start_app(@app)
          args = Enum.map(args, &List.to_string(&1))
          Kernel.CLI.run fn _ -> @module.main(args) end, true
        error ->
          io_error ["Failed to start Elixir.\n", :io_lib.format('error: ~p~n', [error])]
          :erlang.halt(1)
      end
    end
  end

  defp main_body_for(:erlang) do
    quote do
      load_config(@config)
      start_app(@app)
      @module.main(args)
    end
  end
end

defmodule Mix.Tasks.Escript.Build do
  use Mix.Task
  use Bitwise, only_operators: true

  @shortdoc "Builds an escript for the project"
  @recursive true

  @moduledoc ~S"""
  Builds an escript for the project.

  An escript is an executable that can be invoked from the
  command line.  An escript can run on any machine that has
  Erlang installed and by default does not require Elixir to
  be installed, as Elixir is embedded as part of the escript.

  This task guarantees the project and its dependencies are
  compiled and packages them inside an escript.

  ## Command line options

    * `--force`      - forces compilation regardless of modification times
    * `--no-compile` - skips compilation to .beam files

  ## Configuration

  The following option must be specified in your `mix.exs` under `:escript`
  key:

    * `:main_module` - the module to be invoked once the escript starts.
      The module must contain a function named `main/1` that will receive the
      command line arguments as binaries.

  The remaining options can be specified to further customize the escript:

    * `:name` - the name of the generated escript.
      Defaults to app name.

    * `:path` - the path to write the escript to.
      Defaults to app name.

    * `:app` - the app to start with the escript.
      Defaults to app name. Set it to `nil` if no application should
      be started.

    * `:embed_elixir` - if `true` embed elixir and its children apps
      (`ex_unit`, `mix`, etc.) mentioned in the `:applications` list inside the
      `application` function in `mix.exs`.

      Defaults to `true` for Elixir projects, `false` for Erlang projects.

      Note: if you set this to `false` for an Elixir project, you will have to add paths to Elixir's
      `ebin` directories to `ERL_LIBS` environment variable when running the resulting escript, in
      order for the code loader to be able to find `:elixir` application and its children
      applications (if they are used).

    * `:shebang` - shebang interpreter directive used to execute the escript.
      Defaults to `"#! /usr/bin/env escript\n"`.

    * `:comment` - comment line to follow shebang directive in the escript.
      Defaults to `""`.

    * `:emu_args` - emulator arguments to embed in the escript file.
      Defaults to `""`.

  There is one project-level option that affects how the escript is generated:

    * `language: :elixir | :erlang` - set it to `:erlang` for Erlang projects
      managed by mix. Doing so will ensure Elixir is not embedded by default.
      Your app will still be started as part of escript loading, with the
      config used during build.

  ## Example

      defmodule MyApp.Mixfile do
        def project do
          [app: :myapp,
           version: "0.0.1",
           escript: escript]
        end

        def escript do
          [main_module: MyApp.CLI]
        end
      end

  """
  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    Mix.Project.get!
    {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean, compile: :boolean])

    if Keyword.get(opts, :compile, true) do
      Mix.Task.run :compile, args
    end

    project  = Mix.Project.config
    language = Keyword.get(project, :language, :elixir)

    escriptize(project, language, opts[:force])
  end

  defp escriptize(project, language, force) do
    escript_opts = project[:escript] || []

    script_name  = to_string(escript_opts[:name] || project[:app])
    filename     = escript_opts[:path] || script_name
    main         = escript_opts[:main_module]
    app          = Keyword.get(escript_opts, :app, project[:app])
    files        = project_files()

    escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")

    cond do
      !script_name ->
        Mix.raise "Could not generate escript, no name given, " <>
          "set :name escript option or :app in the project settings"

      !main or !Code.ensure_loaded?(main)->
        Mix.raise "Could not generate escript, please set :main_module " <>
          "in your project configuration (under `:escript` option) to a module that implements main/1"

      force || Mix.Utils.stale?(files, [filename]) ->
        beam_paths =
          [files, deps_files(), core_files(escript_opts, language)]
          |> Stream.concat
          |> prepare_beam_paths

        if Keyword.get(project, :consolidate_protocols, project[:build_embedded]) do
          beam_paths =
            Path.wildcard(consolidated_path <> "/*")
            |> prepare_beam_paths(beam_paths)
        end

        tuples = gen_main(escript_mod, main, app, language) ++
                 read_beams(beam_paths)

        case :zip.create 'mem', tuples, [:memory] do
          {:ok, {'mem', zip}} ->
            shebang  = escript_opts[:shebang] || "#! /usr/bin/env escript\n"
            comment  = build_comment(escript_opts[:comment])
            emu_args = build_emu_args(escript_opts[:emu_args], escript_mod)

            script = IO.iodata_to_binary([shebang, comment, emu_args, zip])
            File.mkdir_p!(Path.dirname(filename))
            File.write!(filename, script)
            set_perms(filename)
          {:error, error} ->
            Mix.raise "Error creating escript: #{error}"
        end

        Mix.shell.info "Generated escript #{filename} with MIX_ENV=#{Mix.env}"
        :ok
      true ->
        :noop
    end
  end

  defp project_files() do
    get_files(Mix.Project.app_path)
  end

  defp get_files(app) do
    Path.wildcard("#{app}/ebin/*.{app,beam}") ++
      (Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
  end

  defp set_perms(filename) do
    stat = File.stat!(filename)
    :ok  = File.chmod(filename, stat.mode ||| 0o111)
  end

  defp deps_files() do
    deps = Mix.Dep.loaded(env: Mix.env) || []
    Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
  end

  defp core_files(escript_opts, language) do
    if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
      Enum.flat_map [:elixir|extra_apps()], &app_files/1
    else
      []
    end
  end

  defp extra_apps() do
    mod = Mix.Project.get!

    extra_apps =
      if function_exported?(mod, :application, 0) do
        mod.application[:applications]
      end

    Enum.filter(extra_apps || [], &(&1 in [:eex, :ex_unit, :mix, :iex, :logger]))
  end

  defp app_files(app) do
    case :code.where_is_file('#{app}.app') do
      :non_existing -> Mix.raise "Could not find application #{app}"
      file -> get_files(Path.dirname(Path.dirname(file)))
    end
  end

  defp prepare_beam_paths(paths, dict \\ HashDict.new) do
    paths
    |> Enum.map(&{Path.basename(&1), &1})
    |> Enum.into(dict)
  end

  defp read_beams(items) do
    items
    |> Enum.map(fn {basename, beam_path} ->
      {String.to_char_list(basename), File.read!(beam_path)}
    end)
  end

  defp consolidated_path, do: Mix.Tasks.Compile.Protocols.default_path

  defp build_comment(user_comment) do
    "%% #{user_comment}\n"
  end

  defp build_emu_args(user_args, escript_mod) do
    "%%! -escript main #{escript_mod} #{user_args}\n"
  end

  defp gen_main(name, module, app, language) do
    config =
      if File.regular?("config/config.exs") do
        Macro.escape Mix.Config.read!("config/config.exs")
      else
        []
      end

    module_body = quote do
      @module unquote(module)
      @config unquote(config)
      @app unquote(app)

      @spec main(OptionParser.argv) :: any
      def main(args) do
        unquote(main_body_for(language))
      end

      defp load_config(config) do
        :lists.foreach(fn {app, kw} ->
          :lists.foreach(fn {k, v} ->
            :application.set_env(app, k, v, persistent: true)
          end, kw)
        end, config)
        :ok
      end

      defp start_app(nil) do
        :ok
      end

      defp start_app(app) do
        case :application.ensure_all_started(app) do
          {:ok, _} -> :ok
          {:error, {app, reason}} ->
            formatted_error = case :code.ensure_loaded(Application) do
              {:module, Application} -> Application.format_error(reason)
              {:error, _} -> :io_lib.format('~p', [reason])
            end
            io_error ["Could not start application ",
                      :erlang.atom_to_binary(app, :utf8),
                      ": ", formatted_error, ?\n]
            :erlang.halt(1)
        end
      end

      defp io_error(message) do
        :io.put_chars(:standard_error, message)
      end
    end

    {:module, ^name, binary, _} = Module.create(name, module_body, Macro.Env.location(__ENV__))
    [{'#{name}.beam', binary}]
  end

  defp main_body_for(:elixir) do
    quote do
      erl_version = :erlang.system_info(:otp_release)

      case :string.to_integer(erl_version) do
        {num, _} when num >= 17 -> nil
        _ ->
          io_error ["Incompatible Erlang/OTP release: ", erl_version,
                    ".\nThis escript requires at least Erlang/OTP 17.0.\n"]
          :erlang.halt(1)
      end

      case :application.ensure_all_started(:elixir) do
        {:ok, _} ->
          load_config(@config)
          start_app(@app)
          args = Enum.map(args, &List.to_string(&1))
          Kernel.CLI.run fn _ -> @module.main(args) end, true
        error ->
          io_error ["Failed to start Elixir.\n", :io_lib.format('error: ~p~n', [error])]
          :erlang.halt(1)
      end
    end
  end

  defp main_body_for(:erlang) do
    quote do
      load_config(@config)
      start_app(@app)
      @module.main(args)
    end
  end
end