actual units can take either a string or an int

defmodule Service.Challenge do
  @behaviour Reducer

  import DB.Validation, only: [flip_v1_uuid: 1]

  alias DB.Event
  alias Reducer.State

  @domains [:challenge]
  @types [:start, :stop, :edit, :actual_units, :delete]
  def domains, do: @domains
  def types, do: @types

  @spec call(list(Event.t), State.t) :: State.t
  def call(events, %State{} = state) when is_list(events) do
    {new_model, new_events} = events |> validate()
      |> challenge_reducer_recursive({state.model, []})
    %State{state | model: new_model, new_events: new_events}
  end

  def gen_uuidv1()do
    {uuid_v1, _} = :uuid.get_v1(:uuid.new(self(), :erlang))
    to_string(:uuid.uuid_to_string(uuid_v1))
  end

  def validate(event_list) do
    Enum.filter(event_list, fn(event) -> correct_type?(event) end)
    |> Enum.map( fn(event) -> event_structure(event) end)
    |> sorts_events()
  end

  def uuidv1({_type, event}) do
    event.ordered_id
  end

  def sorts_events(event_list)do
    Enum.sort(event_list, &(uuidv1(&1) < uuidv1(&2)))
  end

  def correct_type?(event) do
    Enum.member?([
      "start",
      "stop",
      "edit",
      "actual_units",
      "delete"], String.downcase(event.type))
  end

  def event_structure(%Event{event_id: event_id, meta: meta} = event) do
    type = String.to_existing_atom(String.downcase(event.type))
    data = %{
      entity_id: event.entity_id,
      domain: event.domain,
      ordered_id: flip_v1_uuid(event_id),
      # TODO:  handle missing timestamp possiblity
      # ** (ArgumentError) argument error
      #    :erlang.binary_to_integer(nil)
      timestamp: String.to_integer(meta["timestamp"]),
      data: meta
    }
    {type, data}
  end

  def challenge_reducer_recursive([], {model, new_events}), do: {model, new_events}
  def challenge_reducer_recursive([event | remaining_events], {model, new_events}) do
    challenge_reducer_recursive(remaining_events, play(event, {model, new_events}))
  end

  def play({type, event}, {model, new_events}) do
    apply(__MODULE__, type, [{type, event}, model, new_events])
  end

  def start_user(user, event, %{"status" => "stopped"} = user_model) do
    {user, user_model
    |> Map.put("status", "running")
    |> Map.put("start_time", event.timestamp)}
  end

  def start_user(user, event, user_model) when map_size(user_model) == 0 do
    {user, user_model
    |> Map.put("status", "running")
    |> Map.put("start_time", event.timestamp)}
  end

  def start_user(user, _event, user_model), do: {user, user_model}

  def start({:start, event}, model, new_events) when model == %{} do
    user_models = Enum.map(event.data["users"], fn(user) -> start_user(to_string(user), event, %{}) end)
    |> Enum.into(%{})

    {model
    |> Map.put("last_played", event.ordered_id)
    |> Map.put("users", user_models)
    |> Map.put("entity_id", event.entity_id)
    |> Map.put("domain", event.domain)
    |> Map.put("challenge_benchmark", event.data["challenge_benchmark"])
    |> Map.put("challenge_type", event.data["challenge_type"])
    |> Map.put("store_id", event.data["store_id"]),
    new_events}
  end

  def start({:start, event}, model, new_events)  do
    user_models = Enum.map(event.data["users"], fn(user) -> start_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    {model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id), new_events}
  end

  def stop_user(user, event, %{"status" => "running", "active_seconds" => active_seconds} = user_model) do
    {user, user_model
    |> Map.put("status", "stopped")
    |> Map.put("active_seconds", ((event.timestamp - user_model["start_time"])/1000) + active_seconds)}
  end

  def stop_user(user, event, %{"status" => "running"} = user_model) do
    {user, user_model
    |> Map.put("status", "stopped")
    |> Map.put("active_seconds", ((event.timestamp - user_model["start_time"])/1000))}
  end

  def stop_user(user, _event, user_model), do: {user, user_model}

  def stop({:stop, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> stop_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    {model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id), new_events}
  end

  def actual_units_user(user, %{:data => %{"units" => units}} = event, %{"status" => "stopped"} = user_model, benchmark) when is_integer(units) do
    units = event.data["units"]
    users = length(event.data["users"])
    actual_units = units / users
    uph = actual_units / (user_model["active_seconds"] / 3600)
    percentage = uph/ benchmark

    new_model = user_model
      |> Map.put("status", "completed")
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
      {user, new_model}
  end

  def actual_units_user(user, event, %{"status" => "stopped"} = user_model, benchmark) do
    units = String.to_integer(event.data["units"])
    users = length(event.data["users"])
    actual_units = units / users
    uph = actual_units / (user_model["active_seconds"] / 3600)
    percentage = uph/ benchmark

    new_model = user_model
      |> Map.put("status", "completed")
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
      {user, new_model}
  end

  def actual_units_user(user, _event, user_model, _benchmark), do: {user, user_model}

  def actual_units({:actual_units, event}, model, new_events ) do
    user_models = Enum.map(event.data["users"], fn(user) -> actual_units_user(to_string(user), event, model["users"][to_string(user)], model["challenge_benchmark"]) end)
    |> Enum.into(model["users"])

    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

  def edit_user(user, _event, %{"status" => "deleted"} = user_model, _benchmark), do: {user, user_model}
  def edit_user(user, %{:data => %{"units" => units}} = event, %{"status" => "stopped"} = user_model, benchmark) when is_integer(units) do
    actual_units = event.data["units"] / length(event.data["users"])
    uph = actual_units / (event.data["duration_min"] / 60)
    percentage = uph/ benchmark

    {user, user_model
      |> Map.put("status", "editted")
      |> Map.put("active_seconds", event.data["duration_min"] * 60)
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
    }
  end

  def edit_user(user, event, user_model, benchmark) do
    actual_units = String.to_integer(event.data["units"]) / length(event.data["users"])
    uph = actual_units / (event.data["duration_min"] / 60)
    percentage = uph/ benchmark

    {user, user_model
      |> Map.put("status", "editted")
      |> Map.put("active_seconds", event.data["duration_min"] * 60)
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
    }
  end

  def edit({:edit, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> edit_user(to_string(user), event, model["users"][to_string(user)], model["challenge_benchmark"]) end)
    |> Enum.into(model["users"])
    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

  def stats_type(challenge_type)do
    case challenge_type do
      "equipment" -> "pre_challenge_transform"
      "apparel" -> "pre_challenge_transform"
      "footwear" -> "pre_challenge_transform"
      "product_refill" -> "refill_challenge_transform"
      _ -> :reject
    end
  end

  def create_stats_event(:reject, _, new_events), do: new_events
  def create_stats_event(type, model, new_events) do
    meta = Map.drop(model, ["last_played", "domain", "entity_id"])
    |> Map.put("challenge_id", model["entity_id"])
    [%Event{
      type: type,
      domain: "transformer",
      realm: "nike",
      entity_id: model["entity_id"],
      meta: meta,
      event_id: gen_uuidv1(),
      remote_ip: "127.0.0.1"
    } | new_events]
  end

  def delete_user(user, _event, user_model) do
    new_model = user_model
      |> Map.put("status", "deleted")
      |> Map.drop(["active_seconds", "actual_units", "uph", "percentage"])
    {user, new_model}
  end

  def delete({:delete, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> delete_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

end

defmodule Service.Challenge do
  @behaviour Reducer

  import DB.Validation, only: [flip_v1_uuid: 1]

  alias DB.Event
  alias Reducer.State

  @domains [:challenge]
  @types [:start, :stop, :edit, :actual_units, :delete]
  def domains, do: @domains
  def types, do: @types

  @spec call(list(Event.t), State.t) :: State.t
  def call(events, %State{} = state) when is_list(events) do
    {new_model, new_events} = events |> validate()
      |> challenge_reducer_recursive({state.model, []})
    %State{state | model: new_model, new_events: new_events}
  end

  def gen_uuidv1()do
    {uuid_v1, _} = :uuid.get_v1(:uuid.new(self(), :erlang))
    to_string(:uuid.uuid_to_string(uuid_v1))
  end

  def validate(event_list) do
    Enum.filter(event_list, fn(event) -> correct_type?(event) end)
    |> Enum.map( fn(event) -> event_structure(event) end)
    |> sorts_events()
  end

  def uuidv1({_type, event}) do
    event.ordered_id
  end

  def sorts_events(event_list)do
    Enum.sort(event_list, &(uuidv1(&1) < uuidv1(&2)))
  end

  def correct_type?(event) do
    Enum.member?([
      "start",
      "stop",
      "edit",
      "actual_units",
      "delete"], String.downcase(event.type))
  end

  def event_structure(%Event{event_id: event_id, meta: meta} = event) do
    type = String.to_existing_atom(String.downcase(event.type))
    data = %{
      entity_id: event.entity_id,
      domain: event.domain,
      ordered_id: flip_v1_uuid(event_id),
      # TODO:  handle missing timestamp possiblity
      # ** (ArgumentError) argument error
      #    :erlang.binary_to_integer(nil)
      timestamp: String.to_integer(meta["timestamp"]),
      data: meta
    }
    {type, data}
  end

  def challenge_reducer_recursive([], {model, new_events}), do: {model, new_events}
  def challenge_reducer_recursive([event | remaining_events], {model, new_events}) do
    challenge_reducer_recursive(remaining_events, play(event, {model, new_events}))
  end

  def play({type, event}, {model, new_events}) do
    apply(__MODULE__, type, [{type, event}, model, new_events])
  end

  def start_user(user, event, %{"status" => "stopped"} = user_model) do
    {user, user_model
    |> Map.put("status", "running")
    |> Map.put("start_time", event.timestamp)}
  end

  def start_user(user, event, user_model) when map_size(user_model) == 0 do
    {user, user_model
    |> Map.put("status", "running")
    |> Map.put("start_time", event.timestamp)}
  end

  def start_user(user, _event, user_model), do: {user, user_model}

  def start({:start, event}, model, new_events) when model == %{} do
    user_models = Enum.map(event.data["users"], fn(user) -> start_user(to_string(user), event, %{}) end)
    |> Enum.into(%{})

    {model
    |> Map.put("last_played", event.ordered_id)
    |> Map.put("users", user_models)
    |> Map.put("entity_id", event.entity_id)
    |> Map.put("domain", event.domain)
    |> Map.put("challenge_benchmark", event.data["challenge_benchmark"])
    |> Map.put("challenge_type", event.data["challenge_type"])
    |> Map.put("store_id", event.data["store_id"]),
    new_events}
  end

  def start({:start, event}, model, new_events)  do
    user_models = Enum.map(event.data["users"], fn(user) -> start_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    {model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id), new_events}
  end

  def stop_user(user, event, %{"status" => "running", "active_seconds" => active_seconds} = user_model) do
    {user, user_model
    |> Map.put("status", "stopped")
    |> Map.put("active_seconds", ((event.timestamp - user_model["start_time"])/1000) + active_seconds)}
  end

  def stop_user(user, event, %{"status" => "running"} = user_model) do
    {user, user_model
    |> Map.put("status", "stopped")
    |> Map.put("active_seconds", ((event.timestamp - user_model["start_time"])/1000))}
  end

  def stop_user(user, _event, user_model), do: {user, user_model}

  def stop({:stop, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> stop_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    {model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id), new_events}
  end

  def actual_units_user(user, event, %{"status" => "stopped"} = user_model, benchmark) do
    units = String.to_integer(event.data["units"])
    users = length(event.data["users"])
    actual_units = units / users
    uph = actual_units / (user_model["active_seconds"] / 3600)
    percentage = uph/ benchmark

    new_model = user_model
      |> Map.put("status", "completed")
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
      {user, new_model}
  end

  def actual_units_user(user, _event, user_model, _benchmark), do: {user, user_model}

  def actual_units({:actual_units, event}, model, new_events ) do
    user_models = Enum.map(event.data["users"], fn(user) -> actual_units_user(to_string(user), event, model["users"][to_string(user)], model["challenge_benchmark"]) end)
    |> Enum.into(model["users"])

    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

  def edit_user(user, _event, %{"status" => "deleted"} = user_model, _benchmark), do: {user, user_model}
  def edit_user(user, event, user_model, benchmark) do
    actual_units = String.to_integer(event.data["units"]) / length(event.data["users"])
    uph = actual_units / (event.data["duration_min"] / 60)
    percentage = uph/ benchmark

    {user, user_model
      |> Map.put("status", "editted")
      |> Map.put("active_seconds", event.data["duration_min"] * 60)
      |> Map.put("actual_units", actual_units)
      |> Map.put("uph", uph)
      |> Map.put("percentage", percentage)
    }
  end

  def edit({:edit, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> edit_user(to_string(user), event, model["users"][to_string(user)], model["challenge_benchmark"]) end)
    |> Enum.into(model["users"])
    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

  def stats_type(challenge_type)do
    case challenge_type do
      "equipment" -> "pre_challenge_transform"
      "apparel" -> "pre_challenge_transform"
      "footwear" -> "pre_challenge_transform"
      "product_refill" -> "refill_challenge_transform"
      _ -> :reject
    end
  end

  def create_stats_event(:reject, _, new_events), do: new_events
  def create_stats_event(type, model, new_events) do
    meta = Map.drop(model, ["last_played", "domain", "entity_id"])
    |> Map.put("challenge_id", model["entity_id"])
    [%Event{
      type: type,
      domain: "transformer",
      realm: "nike",
      entity_id: model["entity_id"],
      meta: meta,
      event_id: gen_uuidv1(),
      remote_ip: "127.0.0.1"
    } | new_events]
  end

  def delete_user(user, _event, user_model) do
    new_model = user_model
      |> Map.put("status", "deleted")
      |> Map.drop(["active_seconds", "actual_units", "uph", "percentage"])
    {user, new_model}
  end

  def delete({:delete, event}, model, new_events) do
    user_models = Enum.map(event.data["users"], fn(user) -> delete_user(to_string(user), event, model["users"][to_string(user)]) end)
    |> Enum.into(model["users"])
    new_model = model
    |> Map.put("users", user_models)
    |> Map.put("last_played", event.ordered_id)
    {new_model, create_stats_event(stats_type(new_model["challenge_type"]), new_model, new_events)}
  end

end


tests updated

tests updated

defmodule IpfsElixirApiTest do
  use ExUnit.Case, async: true
  doctest IpfsConnection

  alias IpfsElixir.Api

  test "get_cmd should return a map when passed a valid key" do
    bin = Api.get_cmd("QmP3LE29sJf8vsnTG8dMeDEcrfoW2cyUG5XVUNKWZxg38i")
    assert is_map(bin)
    assert is_binary(bin.body)
    assert is_map(bin.headers)
    assert bin[:error] == nil
  end

  test "get_cmd should return an error when passed nothing or invalid key" do
    bin = Api.get_cmd("test_case")
    assert is_map(bin)
    assert bin.body["Message"] === "invalid 'ipfs ref' path"
    assert bin.body["Code"] === 0
  end

  ##TODO: add Unit testing
end

defmodule IpfsElixirApiTest do
  use ExUnit.Case, async: true
  doctest IpfsConnection

  import IpfsConnection
  alias IpfsElixir.Api

  test "get_cmd should return a map when passed a valid key" do
    bin = Api.get_cmd("QmP3LE29sJf8vsnTG8dMeDEcrfoW2cyUG5XVUNKWZxg38i")
    assert is_map(bin)
    assert is_binary(bin.body)
    assert is_map(bin.headers)
    assert bin[:error] == nil
  end

  test "get_cmd should return an error when passed nothing or invalid key" do
    bin = Api.get_cmd("test_case")
    assert is_map(bin)
    assert bin.body["Message"] === "invalid 'ipfs ref' path"
    assert bin.body["Code"] === 0
  end

  ##TODO: add Unit testing
end


Included the unit test mentioned in issue #5.

defmodule Sippet.Message.TortureTest do
  use ExUnit.Case, async: true

  alias Sippet.Message, as: Message
  alias Sippet.URI, as: URI

  test "tortuous invite" do
    message = """
    INVITE sip:vivekg@chair-dnrc.example.com;unknownparam SIP/2.0
    TO :
     sip:vivekg@chair-dnrc.example.com ;   tag    = 1918181833n
    from   : "J Rosenberg \\\\\\""       <sip:jdrosen@example.com>
      ;
      tag = 98asjd8
    MaX-fOrWaRdS: 0068
    Call-ID: wsinv.ndaksdj@192.0.2.1
    Content-Length   : 150
    cseq: 0009
      INVITE
    Via  : SIP  /   2.0
     /UDP
        192.0.2.2;branch=390skdjuw
    s :
    NewFangledHeader:   newfangled value
     continued newfangled value
    UnknownHeaderWithUnusualValue: ;;,,;;,;
    Content-Type: application/sdp
    Route:
     <sip:services.example.com;lr;unknownwith=value;unknown-no-value>
    v:  SIP  / 2.0  / TCP     spindle.example.com   ;
      branch  =   z9hG4bK9ikj8  ,
     SIP  /    2.0   / UDP  192.168.255.111   ; branch=
     z9hG4bK30239
    m:"Quoted string \\"\\"" <sip:jdrosen@example.com> ; newparam =
          newvalue ;
      secondparam ; q = 0.33

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method == :invite
    assert request.start_line.request_uri ==
      URI.parse!("sip:vivekg@chair-dnrc.example.com;unknownparam")
    assert request.start_line.version == {2, 0}

    assert elem(request.headers.to, 0) == ""
    assert elem(request.headers.to, 1)
      URI.parse!("sip:vivekg@chair-dnrc.example.com")
    assert elem(request.headers.to, 2) ==
      %{"tag" => "1918181833n"}

    assert elem(request.headers.from, 0) == "J Rosenberg \\\""
    assert elem(request.headers.from, 1) ==
      URI.parse!("sip:jdrosen@example.com")
    assert elem(request.headers.from, 2) ==
      %{"tag" => "98asjd8"}

    assert request.headers.max_forwards == 68

    assert request.headers.call_id == "wsinv.ndaksdj@192.0.2.1"

    assert request.headers.content_length == 150

    assert request.headers.cseq == {9, :invite}

    [first_via|rest] = request.headers.via
    assert first_via ==
      {{2, 0}, :udp, {"192.0.2.2", 5060}, %{"branch" => "390skdjuw"}}

    [second_via|rest] = rest
    assert second_via ==
      {{2, 0}, :tcp, {"spindle.example.com", 5060}, %{"branch" => "z9hG4bK9ikj8"}}

    [third_via] = rest
    assert third_via ==
      {{2, 0}, :udp, {"192.168.255.111", 5060}, %{"branch" => "z9hG4bK30239"}}

    assert request.headers.subject == ""

    assert request.headers["NewFangledHeader"] ==
      ["newfangled value continued newfangled value"]

    assert request.headers["UnknownHeaderWithUnusualValue"] ==
      [";;,,;;,;"]

    assert request.headers.content_type == {{"application", "sdp"}, %{}}

    assert List.first(request.headers.route) ==
      {"", URI.parse!("sip:services.example.com;lr;unknownwith=value;unknown-no-value"), %{}}

    assert List.first(request.headers.contact) ==
      {"Quoted string \"\"", URI.parse!("sip:jdrosen@example.com"),
          %{"newparam" => "newvalue", "secondparam" => "", "q" => "0.33"}}
  end

  test "wide range of characters" do
    message = """
    !interesting-Method0123456789_*+`.%indeed'~ sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*:&it+has=1,weird!*pas$wo~d_too.(doesn't-it)@example.com SIP/2.0
    Via: SIP/2.0/TCP host1.example.com;branch=z9hG4bK-.!%66*_+`'~
    To: "BEL: NUL:\u{0000} DEL:" <sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*@example.com>
    From: token1~` token2'+_ token3*%!.- <sip:mundane@example.com>;fromParam''~+*_!.-%="Ñ\u{20AC}Ð°Ð±Ð¾Ñ\u{2012}Ð°Ñ×ÑÐ¸Ð¹";tag=_token~1'+`*%!-.
    Call-ID: intmeth.word%ZK-!.*_+'@word`~)(><:\/"][?}{
    CSeq: 139122385 !interesting-Method0123456789_*+`.%indeed'~
    Max-Forwards: 255
    extensionHeader-!.%*+_`'~: ï»¿å¤§åé»
    Content-Length: 0

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method ==
      "!interesting-Method0123456789_*+`.%indeed'~"
    assert request.start_line.request_uri ==
      URI.parse!("sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*:&it+has=1,weird!*pas$wo~d_too.(doesn't-it)@example.com")
    assert request.start_line.version == {2, 0}

    assert List.first(request.headers.via) ==
      {{2, 0}, :tcp, {"host1.example.com", 5060}, %{"branch" => "z9hG4bK-.!%66*_+`'~"}}

    assert request.headers.to ==
      {"BEL: NUL:\u{0000} DEL:",
          URI.parse!("sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*@example.com"),
          %{}}

    # Note that the parser will transform parameters into lowercase strings
    assert request.headers.from ==
      {"token1~` token2'+_ token3*%!.-", URI.parse!("sip:mundane@example.com"),
          %{"fromparam''~+*_!.-%" => "Ñ\u{20AC}Ð°Ð±Ð¾Ñ\u{2012}Ð°Ñ×ÑÐ¸Ð¹",
            "tag" => "_token~1'+`*%!-."}}

    assert request.headers.call_id == "intmeth.word%ZK-!.*_+'@word`~)(><:\/\"][?}{"

    assert request.headers.cseq ==
      {139122385, "!interesting-Method0123456789_*+`.%indeed'~"}

    assert request.headers.max_forwards == 255

    assert request.headers["extensionHeader-!.%*+_`'~"] ==
      ["ï»¿å¤§åé»"]

    assert request.headers.content_length == 0
  end

  test "use of % when it is not an escape" do
    message = """
    RE%47IST%45R sip:registrar.example.com SIP/2.0
    To: "%Z%45" <sip:resource@example.com>
    From: "%Z%45" <sip:resource@example.com>;tag=f232jadfj23
    Call-ID: esc02.asdfnqwo34rq23i34jrjasdcnl23nrlknsdf
    Via: SIP/2.0/TCP host.example.com;branch=z9hG4bK209%fzsnel234
    CSeq: 29344 RE%47IST%45R
    Max-Forwards: 70
    Contact: <sip:alias1@host1.example.com>
    C%6Fntact: <sip:alias2@host2.example.com>
    Contact: <sip:alias3@host3.example.com>
    l: 0

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method == "RE%47IST%45R"

    assert elem(request.headers.to, 0) == "%Z%45"

    assert elem(List.first(request.headers.via), 3)["branch"] ==
      "z9hG4bK209%fzsnel234"

    assert elem(request.headers.cseq, 1) == "RE%47IST%45R"

    assert Map.has_key?(request.headers, "C%6Fntact")

    assert length(request.headers.contact) == 2
  end

  test "message with no LWS between display name and <" do
    message = """
    OPTIONS sip:user@example.com SIP/2.0
    To: sip:user@example.com
    From: caller<sip:caller@example.com>;tag=323
    Max-Forwards: 70
    Call-ID: lwsdisp.1234abcd@funky.example.com
    CSeq: 60 OPTIONS
    Via: SIP/2.0/UDP funky.example.com;branch=z9hG4bKkdjuw
    l: 0

    """
    request = Message.parse!(message)

    assert elem(request.headers.from, 0) == "caller"
  end

  test "long values in header fields" do
    message = """
    INVITE sip:user@example.com SIP/2.0
    To: "I have a user name of extremeextremeextremeextremeextremeextremeextremeextremeextremeextreme proportion"<sip:user@example.com:6000;unknownparam1=verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongvalue;longparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename=shortvalue;verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongParameterNameWithNoValue>
    F: sip:amazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallername@example.net;tag=12982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982424;unknownheaderparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename=unknowheaderparamvaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevalue;unknownValuelessparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamname
    Call-ID: longreq.onereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongcallid
    CSeq: 3882340 INVITE
    Unknown-LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLong-Name: unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-value; unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-parameter-name = unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-parameter-value
    Via: SIP/2.0/TCP sip33.example.com
    v: SIP/2.0/TCP sip32.example.com
    V: SIP/2.0/TCP sip31.example.com
    Via: SIP/2.0/TCP sip30.example.com
    ViA: SIP/2.0/TCP sip29.example.com
    VIa: SIP/2.0/TCP sip28.example.com
    VIA: SIP/2.0/TCP sip27.example.com
    via: SIP/2.0/TCP sip26.example.com
    viA: SIP/2.0/TCP sip25.example.com
    vIa: SIP/2.0/TCP sip24.example.com
    vIA: SIP/2.0/TCP sip23.example.com
    V :  SIP/2.0/TCP sip22.example.com
    v :  SIP/2.0/TCP sip21.example.com
    V  : SIP/2.0/TCP sip20.example.com
    v  : SIP/2.0/TCP sip19.example.com
    Via : SIP/2.0/TCP sip18.example.com
    Via  : SIP/2.0/TCP sip17.example.com
    Via: SIP/2.0/TCP sip16.example.com
    Via: SIP/2.0/TCP sip15.example.com
    Via: SIP/2.0/TCP sip14.example.com
    Via: SIP/2.0/TCP sip13.example.com
    Via: SIP/2.0/TCP sip12.example.com
    Via: SIP/2.0/TCP sip11.example.com
    Via: SIP/2.0/TCP sip10.example.com
    Via: SIP/2.0/TCP sip9.example.com
    Via: SIP/2.0/TCP sip8.example.com
    Via: SIP/2.0/TCP sip7.example.com
    Via: SIP/2.0/TCP sip6.example.com
    Via: SIP/2.0/TCP sip5.example.com
    Via: SIP/2.0/TCP sip4.example.com
    Via: SIP/2.0/TCP sip3.example.com
    Via: SIP/2.0/TCP sip2.example.com
    Via: SIP/2.0/TCP sip1.example.com
    Via: SIP/2.0/TCP host.example.com;received=192.0.2.5;branch=verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongbranchvalue
    Max-Forwards: 70
    Contact: <sip:amazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallername@host5.example.net>
    Content-Type: application/sdp
    l: 150

    """
    request = Message.parse!(message)

    assert length(request.headers.via) == 34

    assert request.headers.call_id ==
      "longreq.onereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongcallid"
  end

  test "from with addr-spec like format" do
    options =
      """
      OPTIONS sip:john.doe@example.com SIP/2.0
      from   : sip:jdrosen@example.com;tag = 98asjd8

      """

    request = options |> Message.parse!

    assert elem(request.headers.from, 2) ==
      %{"tag" => "98asjd8"}
  end

end

defmodule Sippet.Message.TortureTest do
  use ExUnit.Case, async: true

  alias Sippet.Message, as: Message
  alias Sippet.URI, as: URI

  test "tortuous invite" do
    message = """
    INVITE sip:vivekg@chair-dnrc.example.com;unknownparam SIP/2.0
    TO :
     sip:vivekg@chair-dnrc.example.com ;   tag    = 1918181833n
    from   : "J Rosenberg \\\\\\""       <sip:jdrosen@example.com>
      ;
      tag = 98asjd8
    MaX-fOrWaRdS: 0068
    Call-ID: wsinv.ndaksdj@192.0.2.1
    Content-Length   : 150
    cseq: 0009
      INVITE
    Via  : SIP  /   2.0
     /UDP
        192.0.2.2;branch=390skdjuw
    s :
    NewFangledHeader:   newfangled value
     continued newfangled value
    UnknownHeaderWithUnusualValue: ;;,,;;,;
    Content-Type: application/sdp
    Route:
     <sip:services.example.com;lr;unknownwith=value;unknown-no-value>
    v:  SIP  / 2.0  / TCP     spindle.example.com   ;
      branch  =   z9hG4bK9ikj8  ,
     SIP  /    2.0   / UDP  192.168.255.111   ; branch=
     z9hG4bK30239
    m:"Quoted string \\"\\"" <sip:jdrosen@example.com> ; newparam =
          newvalue ;
      secondparam ; q = 0.33

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method == :invite
    assert request.start_line.request_uri ==
      URI.parse!("sip:vivekg@chair-dnrc.example.com;unknownparam")
    assert request.start_line.version == {2, 0}

    assert elem(request.headers.to, 0) == ""
    assert elem(request.headers.to, 1)
      URI.parse!("sip:vivekg@chair-dnrc.example.com")
    assert elem(request.headers.to, 2) ==
      %{"tag" => "1918181833n"}

    assert elem(request.headers.from, 0) == "J Rosenberg \\\""
    assert elem(request.headers.from, 1) ==
      URI.parse!("sip:jdrosen@example.com")
    assert elem(request.headers.from, 2) ==
      %{"tag" => "98asjd8"}

    assert request.headers.max_forwards == 68

    assert request.headers.call_id == "wsinv.ndaksdj@192.0.2.1"

    assert request.headers.content_length == 150

    assert request.headers.cseq == {9, :invite}

    [first_via|rest] = request.headers.via
    assert first_via ==
      {{2, 0}, :udp, {"192.0.2.2", 5060}, %{"branch" => "390skdjuw"}}

    [second_via|rest] = rest
    assert second_via ==
      {{2, 0}, :tcp, {"spindle.example.com", 5060}, %{"branch" => "z9hG4bK9ikj8"}}

    [third_via] = rest
    assert third_via ==
      {{2, 0}, :udp, {"192.168.255.111", 5060}, %{"branch" => "z9hG4bK30239"}}

    assert request.headers.subject == ""

    assert request.headers["NewFangledHeader"] ==
      ["newfangled value continued newfangled value"]

    assert request.headers["UnknownHeaderWithUnusualValue"] ==
      [";;,,;;,;"]

    assert request.headers.content_type == {{"application", "sdp"}, %{}}

    assert List.first(request.headers.route) ==
      {"", URI.parse!("sip:services.example.com;lr;unknownwith=value;unknown-no-value"), %{}}

    assert List.first(request.headers.contact) ==
      {"Quoted string \"\"", URI.parse!("sip:jdrosen@example.com"),
          %{"newparam" => "newvalue", "secondparam" => "", "q" => "0.33"}}
  end

  test "wide range of characters" do
    message = """
    !interesting-Method0123456789_*+`.%indeed'~ sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*:&it+has=1,weird!*pas$wo~d_too.(doesn't-it)@example.com SIP/2.0
    Via: SIP/2.0/TCP host1.example.com;branch=z9hG4bK-.!%66*_+`'~
    To: "BEL: NUL:\u{0000} DEL:" <sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*@example.com>
    From: token1~` token2'+_ token3*%!.- <sip:mundane@example.com>;fromParam''~+*_!.-%="Ñ\u{20AC}Ð°Ð±Ð¾Ñ\u{2012}Ð°Ñ×ÑÐ¸Ð¹";tag=_token~1'+`*%!-.
    Call-ID: intmeth.word%ZK-!.*_+'@word`~)(><:\/"][?}{
    CSeq: 139122385 !interesting-Method0123456789_*+`.%indeed'~
    Max-Forwards: 255
    extensionHeader-!.%*+_`'~: ï»¿å¤§åé»
    Content-Length: 0

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method ==
      "!interesting-Method0123456789_*+`.%indeed'~"
    assert request.start_line.request_uri ==
      URI.parse!("sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*:&it+has=1,weird!*pas$wo~d_too.(doesn't-it)@example.com")
    assert request.start_line.version == {2, 0}

    assert List.first(request.headers.via) ==
      {{2, 0}, :tcp, {"host1.example.com", 5060}, %{"branch" => "z9hG4bK-.!%66*_+`'~"}}

    assert request.headers.to ==
      {"BEL: NUL:\u{0000} DEL:",
          URI.parse!("sip:1_unusual.URI~(to-be!sure)&isn't+it$/crazy?,/;;*@example.com"),
          %{}}

    # Note that the parser will transform parameters into lowercase strings
    assert request.headers.from ==
      {"token1~` token2'+_ token3*%!.-", URI.parse!("sip:mundane@example.com"),
          %{"fromparam''~+*_!.-%" => "Ñ\u{20AC}Ð°Ð±Ð¾Ñ\u{2012}Ð°Ñ×ÑÐ¸Ð¹",
            "tag" => "_token~1'+`*%!-."}}

    assert request.headers.call_id == "intmeth.word%ZK-!.*_+'@word`~)(><:\/\"][?}{"

    assert request.headers.cseq ==
      {139122385, "!interesting-Method0123456789_*+`.%indeed'~"}

    assert request.headers.max_forwards == 255

    assert request.headers["extensionHeader-!.%*+_`'~"] ==
      ["ï»¿å¤§åé»"]

    assert request.headers.content_length == 0
  end

  test "use of % when it is not an escape" do
    message = """
    RE%47IST%45R sip:registrar.example.com SIP/2.0
    To: "%Z%45" <sip:resource@example.com>
    From: "%Z%45" <sip:resource@example.com>;tag=f232jadfj23
    Call-ID: esc02.asdfnqwo34rq23i34jrjasdcnl23nrlknsdf
    Via: SIP/2.0/TCP host.example.com;branch=z9hG4bK209%fzsnel234
    CSeq: 29344 RE%47IST%45R
    Max-Forwards: 70
    Contact: <sip:alias1@host1.example.com>
    C%6Fntact: <sip:alias2@host2.example.com>
    Contact: <sip:alias3@host3.example.com>
    l: 0

    """
    request = Message.parse!(message)

    assert Message.request?(request)

    assert request.start_line.method == "RE%47IST%45R"

    assert elem(request.headers.to, 0) == "%Z%45"

    assert elem(List.first(request.headers.via), 3)["branch"] ==
      "z9hG4bK209%fzsnel234"

    assert elem(request.headers.cseq, 1) == "RE%47IST%45R"

    assert Map.has_key?(request.headers, "C%6Fntact")

    assert length(request.headers.contact) == 2
  end

  test "message with no LWS between display name and <" do
    message = """
    OPTIONS sip:user@example.com SIP/2.0
    To: sip:user@example.com
    From: caller<sip:caller@example.com>;tag=323
    Max-Forwards: 70
    Call-ID: lwsdisp.1234abcd@funky.example.com
    CSeq: 60 OPTIONS
    Via: SIP/2.0/UDP funky.example.com;branch=z9hG4bKkdjuw
    l: 0

    """
    request = Message.parse!(message)

    assert elem(request.headers.from, 0) == "caller"
  end

  test "long values in header fields" do
    message = """
    INVITE sip:user@example.com SIP/2.0
    To: "I have a user name of extremeextremeextremeextremeextremeextremeextremeextremeextremeextreme proportion"<sip:user@example.com:6000;unknownparam1=verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongvalue;longparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename=shortvalue;verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongParameterNameWithNoValue>
    F: sip:amazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallername@example.net;tag=12982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982982424;unknownheaderparamnamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamenamename=unknowheaderparamvaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevaluevalue;unknownValuelessparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamnameparamname
    Call-ID: longreq.onereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongcallid
    CSeq: 3882340 INVITE
    Unknown-LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLong-Name: unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-value; unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-parameter-name = unknown-longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong-parameter-value
    Via: SIP/2.0/TCP sip33.example.com
    v: SIP/2.0/TCP sip32.example.com
    V: SIP/2.0/TCP sip31.example.com
    Via: SIP/2.0/TCP sip30.example.com
    ViA: SIP/2.0/TCP sip29.example.com
    VIa: SIP/2.0/TCP sip28.example.com
    VIA: SIP/2.0/TCP sip27.example.com
    via: SIP/2.0/TCP sip26.example.com
    viA: SIP/2.0/TCP sip25.example.com
    vIa: SIP/2.0/TCP sip24.example.com
    vIA: SIP/2.0/TCP sip23.example.com
    V :  SIP/2.0/TCP sip22.example.com
    v :  SIP/2.0/TCP sip21.example.com
    V  : SIP/2.0/TCP sip20.example.com
    v  : SIP/2.0/TCP sip19.example.com
    Via : SIP/2.0/TCP sip18.example.com
    Via  : SIP/2.0/TCP sip17.example.com
    Via: SIP/2.0/TCP sip16.example.com
    Via: SIP/2.0/TCP sip15.example.com
    Via: SIP/2.0/TCP sip14.example.com
    Via: SIP/2.0/TCP sip13.example.com
    Via: SIP/2.0/TCP sip12.example.com
    Via: SIP/2.0/TCP sip11.example.com
    Via: SIP/2.0/TCP sip10.example.com
    Via: SIP/2.0/TCP sip9.example.com
    Via: SIP/2.0/TCP sip8.example.com
    Via: SIP/2.0/TCP sip7.example.com
    Via: SIP/2.0/TCP sip6.example.com
    Via: SIP/2.0/TCP sip5.example.com
    Via: SIP/2.0/TCP sip4.example.com
    Via: SIP/2.0/TCP sip3.example.com
    Via: SIP/2.0/TCP sip2.example.com
    Via: SIP/2.0/TCP sip1.example.com
    Via: SIP/2.0/TCP host.example.com;received=192.0.2.5;branch=verylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongbranchvalue
    Max-Forwards: 70
    Contact: <sip:amazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallernameamazinglylongcallername@host5.example.net>
    Content-Type: application/sdp
    l: 150

    """
    request = Message.parse!(message)

    assert length(request.headers.via) == 34

    assert request.headers.call_id ==
      "longreq.onereallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallyreallylongcallid"
  end
end


define object

define object

defmodule Core.PetitionController do
  use Core.Web, :controller

  def get(conn, params = %{"petition" => petition}) do
    object = Cosmic.get(petition)
    render_petition(conn, params, object)
  end

  # Match petition exists and is proper brand
  defp render_petition(conn, params, object = %{"metadata" => %{
    "brands" => brands,
    "visibility" => visibility
  }}) do
    brand = Keyword.get(GlobalOpts.get(conn, params), :brand)

    if Enum.member?(brands, brand) do
      case visibility do
        "Draft" -> do_render_draft(conn, params, object)
        "Published" -> do_render_petition(conn, params, object)
      end
    else
      redirect_home(conn, params)
    end
  end

  # Match petition is nil
  defp render_petition(conn, params, nil) do
    render conn, "404.html", GlobalOpts.get(conn, params)
  end

  defp do_render_draft(conn, params, object) do
    if Map.has_key?(params, "draft") do
      do_render_petition(conn, params, object)
    else
      redirect_home(conn, params)
    end
  end

  # Extract and render petition
  defp do_render_petition(conn, params, object = %{
    "slug" => slug,
    "content" => content,
    "metadata" => metadata = %{
      "title" => title,
      "sign_button_text" => sign_button_text,
      "post_sign_text" => post_sign_text,
      "background_image" => %{
        "imgix_url" => background_image
      }
    }
  }) do

    count =
      if Map.has_key?(metadata, "count") && metadata["count"] != "" do
        metadata["count"] |> format_count()
      end

    target =
      if count do
        Enum.max([round((count * 2) / 25_000) * 25_000, 25_000])
      end

    progress =
      if count do
        count / target * 100
      end

    og_description = HtmlSanitizeEx.strip_tags(content)

    share_image = URI.encode(get_in(object, ["metadata", "share_image", "imgix_url"]) || background_image)
    background_image = URI.encode(background_image)

    render conn, "petition.html",
      [slug: slug, title: title, content: content, sign_button_text: sign_button_text,
       post_sign_text: post_sign_text, background_image: background_image, share_image: share_image,
       no_footer: true, signed: false, count: pretty_num(count), target: pretty_num(target),
       progress: pretty_num(progress), banner: background_image, description: og_description] ++ GlobalOpts.get(conn, params)
  end

  defp format_count(""), do: nil
  defp format_count(count) when is_integer(count), do: count
  defp format_count(count) when is_binary(count), do: count |> Integer.parse() |> Tuple.to_list() |> List.first()
  defp pretty_num(nil), do: nil
  defp pretty_num(n), do: n |> Number.Delimit.number_to_delimited(precision: 0)

  def post(conn, params = %{"petition" => petition, "name" => name, "email" => email, "zip" => zip}) do
    global_opts = GlobalOpts.get(conn, params)

    object = %{"slug" => slug,
      "content" => content,
      "title" => admin_title,
      "metadata" => %{
        "title" => title,
        "sign_button_text" => sign_button_text,
        "post_sign_text" => post_sign_text,
        "tweet_template" => tweet_template,
        "background_image" => %{
          "imgix_url" => background_image
        }
      }
    } = Cosmic.get(petition)

    url = "https://#{conn.host}/petition/#{slug}"
    twitter_query = URI.encode_query([text: tweet_template, url: url])
    twitter_href = "https://twitter.com/intent/tweet?#{twitter_query}"

    fb_query = URI.encode_query([u: url])
    fb_href = "https://www.facebook.com/sharer/sharer.php?#{fb_query}&src=sdkpreparse"

    # Get person's id / create them
    names = name |> String.trim() |> String.split(" ")
    first_name = List.first(names)
    last_name = if length(names) > 1 do
      List.last(names)
    else
      ""
    end

    %{"id" => id} = Nb.People.push(%{
      "email" => email,
      "first_name" => first_name,
      "last_name" => last_name,
      "mailing_address" => %{"zip" => zip}
    })

    # Add the petition signed tag
    brand = Keyword.get(global_opts, :brand)
    source = case brand do
      "jd" -> "Justice Democrats"
      "bnc" -> "Brand New Congress"
    end

    tags = ["Action: Signed Petition: #{source}: #{admin_title}"] ++
      if Map.has_key?(params, "ref") do
        ["Action: Signed Petition: #{source}: #{admin_title}: #{params["ref"]}"]
      else
        []
      end

    Nb.People.add_tags(id, tags)

    share_image = URI.encode(get_in(object, ["metadata", "share_image", "imgix_url"]) || background_image)
    background_image = URI.encode(background_image)

    render conn, "petition.html",
      [slug: slug, title: title, content: content, sign_button_text: sign_button_text,
       post_sign_text: post_sign_text, background_image: background_image,
       twitter_href: twitter_href, fb_href: fb_href, no_footer: true, url: url,
       signed: true] ++ GlobalOpts.get(conn, params)
  end

  def redirect_home(conn, params) do
    url = case (conn |> GlobalOpts.get(params) |> Keyword.get(:brand)) do
      "jd" -> "https://justicedemocrats.com"
      "bnc" -> "https://brandnewcongress.org"
    end
    redirect(conn, external: url)
  end
end

defmodule Core.PetitionController do
  use Core.Web, :controller

  def get(conn, params = %{"petition" => petition}) do
    object = Cosmic.get(petition)
    render_petition(conn, params, object)
  end

  # Match petition exists and is proper brand
  defp render_petition(conn, params, object = %{"metadata" => %{
    "brands" => brands,
    "visibility" => visibility
  }}) do
    brand = Keyword.get(GlobalOpts.get(conn, params), :brand)

    if Enum.member?(brands, brand) do
      case visibility do
        "Draft" -> do_render_draft(conn, params, object)
        "Published" -> do_render_petition(conn, params, object)
      end
    else
      redirect_home(conn, params)
    end
  end

  # Match petition is nil
  defp render_petition(conn, params, nil) do
    render conn, "404.html", GlobalOpts.get(conn, params)
  end

  defp do_render_draft(conn, params, object) do
    if Map.has_key?(params, "draft") do
      do_render_petition(conn, params, object)
    else
      redirect_home(conn, params)
    end
  end

  # Extract and render petition
  defp do_render_petition(conn, params, object = %{
    "slug" => slug,
    "content" => content,
    "metadata" => metadata = %{
      "title" => title,
      "sign_button_text" => sign_button_text,
      "post_sign_text" => post_sign_text,
      "background_image" => %{
        "imgix_url" => background_image
      }
    }
  }) do

    count =
      if Map.has_key?(metadata, "count") && metadata["count"] != "" do
        metadata["count"] |> format_count()
      end

    target =
      if count do
        Enum.max([round((count * 2) / 25_000) * 25_000, 25_000])
      end

    progress =
      if count do
        count / target * 100
      end

    og_description = HtmlSanitizeEx.strip_tags(content)

    share_image = URI.encode(get_in(object, ["metadata", "share_image", "imgix_url"]) || background_image)
    background_image = URI.encode(background_image)

    render conn, "petition.html",
      [slug: slug, title: title, content: content, sign_button_text: sign_button_text,
       post_sign_text: post_sign_text, background_image: background_image, share_image: share_image,
       no_footer: true, signed: false, count: pretty_num(count), target: pretty_num(target),
       progress: pretty_num(progress), banner: background_image, description: og_description] ++ GlobalOpts.get(conn, params)
  end

  defp format_count(""), do: nil
  defp format_count(count) when is_integer(count), do: count
  defp format_count(count) when is_binary(count), do: count |> Integer.parse() |> Tuple.to_list() |> List.first()
  defp pretty_num(nil), do: nil
  defp pretty_num(n), do: n |> Number.Delimit.number_to_delimited(precision: 0)

  def post(conn, params = %{"petition" => petition, "name" => name, "email" => email, "zip" => zip}) do
    global_opts = GlobalOpts.get(conn, params)

    %{"slug" => slug,
      "content" => content,
      "title" => admin_title,
      "metadata" => %{
        "title" => title,
        "sign_button_text" => sign_button_text,
        "post_sign_text" => post_sign_text,
        "tweet_template" => tweet_template,
        "background_image" => %{
          "imgix_url" => background_image
        }
      }
    } = Cosmic.get(petition)

    url = "https://#{conn.host}/petition/#{slug}"
    twitter_query = URI.encode_query([text: tweet_template, url: url])
    twitter_href = "https://twitter.com/intent/tweet?#{twitter_query}"

    fb_query = URI.encode_query([u: url])
    fb_href = "https://www.facebook.com/sharer/sharer.php?#{fb_query}&src=sdkpreparse"

    # Get person's id / create them
    names = name |> String.trim() |> String.split(" ")
    first_name = List.first(names)
    last_name = if length(names) > 1 do
      List.last(names)
    else
      ""
    end

    %{"id" => id} = Nb.People.push(%{
      "email" => email,
      "first_name" => first_name,
      "last_name" => last_name,
      "mailing_address" => %{"zip" => zip}
    })

    # Add the petition signed tag
    brand = Keyword.get(global_opts, :brand)
    source = case brand do
      "jd" -> "Justice Democrats"
      "bnc" -> "Brand New Congress"
    end

    tags = ["Action: Signed Petition: #{source}: #{admin_title}"] ++
      if Map.has_key?(params, "ref") do
        ["Action: Signed Petition: #{source}: #{admin_title}: #{params["ref"]}"]
      else
        []
      end

    Nb.People.add_tags(id, tags)

    share_image = URI.encode(get_in(object, ["metadata", "share_image", "imgix_url"]) || background_image)
    background_image = URI.encode(background_image)

    render conn, "petition.html",
      [slug: slug, title: title, content: content, sign_button_text: sign_button_text,
       post_sign_text: post_sign_text, background_image: background_image,
       twitter_href: twitter_href, fb_href: fb_href, no_footer: true, url: url,
       signed: true] ++ GlobalOpts.get(conn, params)
  end

  def redirect_home(conn, params) do
    url = case (conn |> GlobalOpts.get(params) |> Keyword.get(:brand)) do
      "jd" -> "https://justicedemocrats.com"
      "bnc" -> "https://brandnewcongress.org"
    end
    redirect(conn, external: url)
  end
end


Fix dependency

Fix dependency

defmodule Core.QuestionController do
  use Core.Web, :controller
  alias Core.Question
  alias Core.QuestionTag

  plug :find when action in [:update, :delete, :show]
  plug Core.Auth, %{key: :question,
                    type: "questions"} when action in [:create, :update, :delete]

  def index(conn, _params) do
    questions = Repo.all(Question)
    render(conn, "index.json", questions: questions)
  end

  def create(conn, question_params) do
    changeset = Question.changeset(%Question{}, question_params)

    case insert_question_and_tags(changeset) do
      {:ok, question} ->
        question = question
        |> Repo.preload(:question_tags)

        conn
        |> put_status(:created)
        |> put_resp_header("location", question_path(conn, :show, question))
        |> render("show.json", question: question)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Core.ChangesetView, "error.json", changeset: changeset)
    end
  end

  def show(conn, _) do
    question = conn.assigns[:question]
    |> Repo.preload(:question_tags)

    render(conn, "show.json", question: question)
  end

  def update(conn, question_params) do
    question = conn.assigns[:question]
    changeset = Question.changeset(question, question_params)

    case Repo.update(changeset) do
      {:ok, question} ->
        question = question
        |> Repo.preload(:question_tags)

        render(conn, "show.json", question: question)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Core.ChangesetView, "error.json", changeset: changeset)
    end
  end

  def delete(conn, _) do
    question = conn.assigns[:question]

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

    send_resp(conn, :no_content, "")
  end

  defp find(conn = %Plug.Conn{params: %{"id" => id}}, _opts) do
    case Repo.get(Question, id) do
      nil ->
        conn
        |> put_status(:not_found)
        |> render(Core.ErrorView, "404.json", %{})
        |> halt()
      question ->
        conn
        |> assign(:question, question)
    end
  end

  defp insert_question_and_tags(changeset) do
    answer = Ecto.Changeset.get_field(changeset, :answer)

    Repo.transaction(fn ->
      result = Repo.insert(changeset)
      |> add_answer(answer)

      case result do
        {:error, changeset} ->
          Repo.rollback(changeset)

        {:ok, question, tags} ->
          Map.merge(question, %{tags: tags})
      end
    end)
  end

  # Add the answer to a question
  #
  # Returns a {:ok, tags} or {:error, changeset} tuple
  defp add_answer({:error, changeset}, _), do: {:error, changeset}
  defp add_answer({:ok, question}, answer) do
    answer
    |> Enum.map(fn item ->
      QuestionTag.changeset(%QuestionTag{}, %{tag_id: item["tag"],
                                              required: item["required"],
                                              question_id: question.id})
    end)
    |> Enum.reduce({:ok, []}, add_tag(question))
  end

  # Add a tag into a question
  #
  # Returns an arity-2 function that reduces a list of changesets reducing it
  # to a single {:ok, tags} or {:error, changeset} tuple
  defp add_tag(question), do: fn
    (_, {:error, changeset}) ->
      {:error, changeset}

    (changeset = %{valid?: valid}, {:ok, _, _}) when not valid ->
      {:error, changeset}

    (changeset, {:ok, tags}) ->
      case Repo.insert(changeset) do
        {:ok, tag} ->
          {:ok, [tag|tags]}

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

defmodule Core.QuestionController do
  use Core.Web, :controller
  alias Core.Question

  plug :find when action in [:update, :delete, :show]
  plug Core.Auth, %{key: :question,
                    type: "questions"} when action in [:create, :update, :delete]

  def index(conn, _params) do
    questions = Repo.all(Question)
    render(conn, "index.json", questions: questions)
  end

  def create(conn, question_params) do
    changeset = Question.changeset(%Question{}, question_params)

    case insert_question_and_tags(changeset) do
      {:ok, question} ->
        question = question
        |> Repo.preload(:question_tags)

        conn
        |> put_status(:created)
        |> put_resp_header("location", question_path(conn, :show, question))
        |> render("show.json", question: question)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Core.ChangesetView, "error.json", changeset: changeset)
    end
  end

  def show(conn, _) do
    question = conn.assigns[:question]
    |> Repo.preload(:question_tags)

    render(conn, "show.json", question: question)
  end

  def update(conn, question_params) do
    question = conn.assigns[:question]
    changeset = Question.changeset(question, question_params)

    case Repo.update(changeset) do
      {:ok, question} ->
        question = question
        |> Repo.preload(:question_tags)

        render(conn, "show.json", question: question)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Core.ChangesetView, "error.json", changeset: changeset)
    end
  end

  def delete(conn, _) do
    question = conn.assigns[:question]

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

    send_resp(conn, :no_content, "")
  end

  defp find(conn = %Plug.Conn{params: %{"id" => id}}, _opts) do
    case Repo.get(Question, id) do
      nil ->
        conn
        |> put_status(:not_found)
        |> render(Core.ErrorView, "404.json", %{})
        |> halt()
      question ->
        conn
        |> assign(:question, question)
    end
  end

  defp insert_question_and_tags(changeset) do
    answer = Ecto.Changeset.get_field(changeset, :answer)

    Repo.transaction(fn ->
      result = Repo.insert(changeset)
      |> add_answer(answer)

      case result do
        {:error, changeset} ->
          Repo.rollback(changeset)

        {:ok, question, tags} ->
          Map.merge(question, %{tags: tags})
      end
    end)
  end

  # Add the answer to a question
  #
  # Returns a {:ok, tags} or {:error, changeset} tuple
  defp add_answer({:error, changeset}, _), do: {:error, changeset}
  defp add_answer({:ok, question}, answer) do
    answer
    |> Enum.map(fn item ->
      QuestionTag.changeset(%QuestionTag{}, %{tag_id: item["tag"],
                                              required: item["required"],
                                              question_id: question.id})
    end)
    |> Enum.reduce({:ok, []}, add_tag(question))
  end

  # Add a tag into a question
  #
  # Returns an arity-2 function that reduces a list of changesets reducing it
  # to a single {:ok, tags} or {:error, changeset} tuple
  defp add_tag(question), do: fn
    (_, {:error, changeset}) ->
      {:error, changeset}

    (changeset = %{valid?: valid}, {:ok, _, _}) when not valid ->
      {:error, changeset}

    (changeset, {:ok, tags}) ->
      case Repo.insert(changeset) do
        {:ok, tag} ->
          {:ok, [tag|tags]}

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


Fix mtg json links

Fix mtg json links

HTTPoison.start
case HTTPoison.get("https://mtgjson.com/json/AllSets.json") do
  {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> File.write!("profile/AllSets.json", body)
end
case HTTPoison.get("https://mtgjson.com/json/AllSets-x.json") do
  {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> File.write!("profile/AllSets-x.json", body)
end

HTTPoison.start
case HTTPoison.get("http://mtgjson.com/json/AllSets.json") do
  {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> File.write!("profile/AllSets.json", body)
end
case HTTPoison.get("http://mtgjson.com/json/AllSets-x.json") do
  {:ok, %HTTPoison.Response{body: body, status_code: 200}} -> File.write!("profile/AllSets-x.json", body)
end


Clean up the example a bit

Clean up the example a bit

#include <vistk/pipeline/process.h>

#include <boost/scoped_ptr.hpp>

using namespace vistk;

class compare_string_process
  : public process
{
  public:
    compare_string_process(config_t const& config);
    ~compare_string_process();

    void _configure();
    void _step();
  private:
    class priv;
    boost::scoped_ptr<priv> d;
};

class compare_string_process::priv
{
  public:
    priv(bool icase);
    ~priv();

    bool const ignore_case;

    static config::key_t const config_icase;
    static config::value_t const default_icase;
    static port_t const port_string1;
    static port_t const port_string2;
    static port_t const port_output;
};

config::key_t const compare_string_process::priv::config_icase = config::key_t("ignore_case");
config::value_t const compare_string_process::priv::default_icase = config::value_t("false");
process::port_t const compare_string_process::priv::port_string1 = port_t("string1");
process::port_t const compare_string_process::priv::port_string2 = port_t("string2");
process::port_t const compare_string_process::priv::port_output = port_t("are_same");

compare_string_process
::compare_string_process(config_t const& config)
  : process(config)
{
  declare_configuration_key(
    priv::config_icase,
    priv::default_icase,
    config::description_t("If set to \'true\', compares strings case insensitively."));

  port_flags_t required;

  required.insert(flag_required);

  declare_input_port(
    priv::port_string1,
    "string",
    required,
    port_description_t("The first string to compare."));
  declare_input_port(
    priv::port_string2,
    "string",
    required,
    port_description_t("The second string to compare."));
  declare_output_port(
    priv::port_output,
    "bool",
    required,
    port_description_t("Sends \'true\' if the strings were the same."));
}

compare_string_process::priv
::priv(bool icase)
  : ignore_case(icase)
{
}

void
compare_string_process
::_configure()
{
  // Configure the process.
  {
    bool const icase = config_value<bool>(priv::config_icase);

    d.reset(new priv(icase));
  }
}

#include <boost/algorithm/string/predicate.hpp>

void
compare_string_process
::_step()
{
  std::string const str1 = grab_from_port_as<std::string>(priv::port_string1);
  std::string const str2 = grab_from_port_as<std::string>(priv::port_string2);

  bool cmp = (str1 == str2);

  if (!cmp && d->ignore_case)
  {
    cmp = boost::iequals(str1, str2);
  }

  push_to_port_as<bool>(priv::port_output, cmp);

  process::_step();
}

compare_string_process
::~compare_string_process()
{
}

compare_string_process::priv
::~priv()
{
}

void
register_processes()
{
  static process_registry::module_t const module_name = process_registry::module_t("example_processes");

  process_registry_t const registry = process_registry::self();

  if (registry->is_module_loaded(module_name))
  {
    return;
  }

  registry->register_process("compare_string", "Compare strings", create_process<compare_string_process>);
}

#include <vistk/pipeline/process.h>

#include <boost/scoped_ptr.hpp>

using namespace vistk;

class compare_string_process
  : public process
{
  public:
    compare_string_process(config_t const& config);
    ~compare_string_process();

    void _configure();
    void _step();
  private:
    class priv;
    boost::scoped_ptr<priv> d;
};

class compare_string_process::priv
{
  public:
    priv(bool icase);
    ~priv();

    bool const ignore_case;

    static config::key_t const config_icase;
    static config::value_t const default_icase;
    static port_t const port_string1;
    static port_t const port_string2;
    static port_t const port_output;
};

config::key_t const compare_string_process::priv::config_icase = config::key_t("ignore_case");
config::value_t const compare_string_process::priv::default_icase = config::value_t("false");
process::port_t const compare_string_process::priv::port_string1 = process::port_t("string1");
process::port_t const compare_string_process::priv::port_string2 = process::port_t("string2");
process::port_t const compare_string_process::priv::port_output = process::port_t("are_same");

compare_string_process
::compare_string_process(config_t const& config)
  : process(config)
{
  declare_configuration_key(
    priv::config_icase,
    priv::default_icase,
    config::description_t("If set to \'true\', compares strings case insensitively."));

  port_flags_t required;

  required.insert(flag_required);

  declare_input_port(
    priv::port_string1,
    "string",
    required,
    port_description_t("The first string to compare."));
  declare_input_port(
    priv::port_string2,
    "string",
    required,
    port_description_t("The second string to compare."));
  declare_output_port(
    priv::port_output,
    "bool",
    required,
    port_description_t("Sends \'true\' if the strings were the same."));
}

compare_string_process::priv
::priv(bool icase)
  : ignore_case(icase)
{
}

void
compare_string_process
::_configure()
{
  // Configure the process.
  {
    bool const icase = config_value<bool>(priv::config_icase);

    d.reset(new priv(icase));
  }
}

#include <boost/algorithm/string/predicate.hpp>

void
compare_string_process
::_step()
{
  std::string const str1 = grab_from_port_as<std::string>(priv::port_string1);
  std::string const str2 = grab_from_port_as<std::string>(priv::port_string2);

  bool cmp = (str1 == str2);

  if (!cmp && d->ignore_case)
  {
    cmp = boost::iequals(str1, str2);
  }

  push_to_port_as<bool>(priv::port_output, cmp);

  process::_step();
}

compare_string_process
::~compare_string_process()
{
}

compare_string_process::priv
::~priv()
{
}

void
register_processes()
{
  static process_registry::module_t const module_name = process_registry::module_t("example_processes");

  process_registry_t const registry = process_registry::self();

  if (registry->is_module_loaded(module_name))
  {
    return;
  }

  registry->register_process("compare_string", "Compare strings", create_process<compare_string_process>);
}


Don't output if there is no doc, closes #1025

defmodule IEx.Introspection do
  # Convenience helpers for showing docs, specs and types
  # from modules. Invoked directly from IEX.Helpers.
  @moduledoc false

  @doc false
  def h(module) when is_atom(module) do
    case Code.ensure_loaded(module) do
      { :module, _ } ->
        case module.__info__(:moduledoc) do
          { _, binary } when is_binary(binary) ->
            IO.puts IO.ANSI.escape("%{yellow}# #{inspect module}\n")
            IO.write IO.ANSI.escape_fragment("%{yellow}") <> binary <> IO.ANSI.escape_fragment("%{reset}")
          { _, _ } ->
            IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module} have been found")
          _ ->
            IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
        end
      { :error, reason } ->
        IO.puts IO.ANSI.escape("%{red}Could not load module #{inspect module}: #{reason}")
    end
  end

  def h(_) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  @doc false
  def h(modules, function) when is_list(modules) and is_atom(function) do
    result =
      Enum.reduce modules, :not_found, fn
        module, :not_found -> h_mod_fun(module, function)
        _module, acc -> acc
      end

    unless result == :ok, do:
      IO.puts IO.ANSI.escape("%{red}No docs for #{function} have been found")

    :ok
  end

  def h(module, function) when is_atom(module) and is_atom(function) do
    case h_mod_fun(module, function) do
      :ok ->
        :ok
      :no_docs ->
        IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
      :not_found ->
        IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  def h(function, arity) when is_atom(function) and is_integer(arity) do
    h([__MODULE__, Kernel, Kernel.SpecialForms], function, arity)
  end

  def h(_, _) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  defp h_mod_fun(mod, fun) when is_atom(mod) and is_atom(fun) do
    if docs = mod.__info__(:docs) do
      result = lc { {f, arity}, _line, _type, _args, doc } inlist docs, fun == f, doc != false do
        h(mod, fun, arity)
        IO.puts ""
      end

      if result != [], do: :ok, else: :not_found
    else
      :no_docs
    end
  end

  @doc false
  def h(modules, function, arity) when is_list(modules) and is_atom(function) and is_integer(arity) do
    result =
      Enum.reduce modules, :not_found, fn
        module, :not_found -> h_mod_fun_arity(module, function, arity)
        _module, acc -> acc
      end

    unless result == :ok, do:
      IO.puts IO.ANSI.escape("%{red}No docs for #{function}/#{arity} have been found")

    :ok
  end

  def h(module, function, arity) when is_atom(module) and is_atom(function) and is_integer(arity) do
    case h_mod_fun_arity(module, function, arity) do
      :ok ->
        :ok
      :no_docs ->
        IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
      :not_found ->
        IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module}.#{function}/#{arity} have been found")
    end

    :ok
  end

  def h(_, _, _) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  defp h_mod_fun_arity(mod, fun, arity) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
    if docs = mod.__info__(:docs) do
      doc =
        cond do
          d = find_doc(docs, fun, arity)         -> d
          d = find_default_doc(docs, fun, arity) -> d
          true                                   -> nil
        end

      if doc do
        print_doc(doc)
        :ok
      else
        :not_found
      end
    else
      :no_docs
    end
  end

  defp find_doc(docs, function, arity) do
    if doc = List.keyfind(docs, { function, arity }, 0) do
      case elem(doc, 4) do
        false -> nil
        _ -> doc
      end
    end
  end

  defp find_default_doc(docs, function, min) do
    Enum.find docs, fn(doc) ->
      case elem(doc, 0) do
        { ^function, max } when max > min ->
          defaults = Enum.count elem(doc, 3), match?({ ://, _, _ }, &1)
          min + defaults >= max
        _ ->
          false
      end
    end
  end

  defp print_doc({ { fun, _ }, _line, kind, args, doc }) do
    args = Enum.map_join(args, ", ", print_doc_arg(&1))
    IO.puts IO.ANSI.escape("%{yellow}* #{kind} #{fun}(#{args})\n")
    if doc, do: IO.write IO.ANSI.escape("%{yellow} #{doc}")
  end

  defp print_doc_arg({ ://, _, [left, right] }) do
    print_doc_arg(left) <> " // " <> Macro.to_binary(right)
  end

  defp print_doc_arg({ var, _, _ }) do
    atom_to_binary(var)
  end

  @doc false
  def t(module) do
    types = lc type inlist Kernel.Typespec.beam_types(module), do: print_type(type)

    if types == [] do
      IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module} have been found")
    end

    :ok
  end

  @doc false
  def t(module, type) when is_atom(type) do
    types = lc {_, {t, _, _args}} = typespec inlist Kernel.Typespec.beam_types(module),
               t == type do
      print_type(typespec)
      typespec
    end

    if types == [] do
       IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module}.#{type} have been found")
    end

    :ok
  end

  @doc false
  def t(module, type, arity) do
    types = lc {_, {t, _, args}} = typespec inlist Kernel.Typespec.beam_types(module),
               length(args) == arity and t == type, do: typespec

    case types do
     [] ->
       IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module}.#{type}/#{arity} have been found")
     [type] ->
       print_type(type)
    end

    :ok
  end

  @doc false
  def s(module) do
    specs = lc spec inlist beam_specs(module), do: print_spec(spec)

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module} have been found")
    end

    :ok
  end

  @doc false
  def s(module, function) when is_atom(function) do
    specs = lc {_kind, {{f, _arity}, _spec}} = spec inlist beam_specs(module),
               f == function do
      print_spec(spec)
      spec
    end

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  @doc false
  def s(module, function, arity) do
    specs = lc {_kind, {{f, a}, _spec}} = spec inlist beam_specs(module),
               f == function and a == arity do
      print_spec(spec)
      spec
    end

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  defp beam_specs(module) do
    specs = Enum.map(Kernel.Typespec.beam_specs(module), {:spec, &1})
    callbacks = Enum.map(Kernel.Typespec.beam_callbacks(module), {:callback, &1})
    List.concat(specs, callbacks)
  end

  defp print_type({ kind, type }) do
    ast = Kernel.Typespec.type_to_ast(type)
    IO.puts IO.ANSI.escape("%{yellow}@#{kind} #{Macro.to_binary(ast)}")
    true
  end

  defp print_spec({kind, { { name, _arity }, specs }}) do
    Enum.each specs, fn(spec) ->
      binary = Macro.to_binary Kernel.Typespec.spec_to_ast(name, spec)
      IO.puts IO.ANSI.escape("%{yellow}@#{kind} #{binary}")
    end
    true
  end
end

defmodule IEx.Introspection do
  # Convenience helpers for showing docs, specs and types
  # from modules. Invoked directly from IEX.Helpers.
  @moduledoc false

  @doc false
  def h(module) when is_atom(module) do
    case Code.ensure_loaded(module) do
      { :module, _ } ->
        case module.__info__(:moduledoc) do
          { _, binary } when is_binary(binary) ->
            IO.puts IO.ANSI.escape("%{yellow}# #{inspect module}\n")
            IO.write IO.ANSI.escape_fragment("%{yellow}") <> binary <> IO.ANSI.escape_fragment("%{reset}")
          { _, _ } ->
            IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module} have been found")
          _ ->
            IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
        end
      { :error, reason } ->
        IO.puts IO.ANSI.escape("%{red}Could not load module #{inspect module}: #{reason}")
    end
  end

  def h(_) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  @doc false
  def h(modules, function) when is_list(modules) and is_atom(function) do
    result =
      Enum.reduce modules, :not_found, fn
        module, :not_found -> h_mod_fun(module, function)
        _module, acc -> acc
      end

    unless result == :ok, do:
      IO.puts IO.ANSI.escape("%{red}No docs for #{function} have been found")

    :ok
  end

  def h(module, function) when is_atom(module) and is_atom(function) do
    case h_mod_fun(module, function) do
      :ok ->
        :ok
      :no_docs ->
        IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
      :not_found ->
        IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  def h(function, arity) when is_atom(function) and is_integer(arity) do
    h([__MODULE__, Kernel, Kernel.SpecialForms], function, arity)
  end

  def h(_, _) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  defp h_mod_fun(mod, fun) when is_atom(mod) and is_atom(fun) do
    if docs = mod.__info__(:docs) do
      result = lc { {f, arity}, _line, _type, _args, doc } inlist docs, fun == f, doc != false do
        h(mod, fun, arity)
        IO.puts ""
      end

      if result != [], do: :ok, else: :not_found
    else
      :no_docs
    end
  end

  @doc false
  def h(modules, function, arity) when is_list(modules) and is_atom(function) and is_integer(arity) do
    result =
      Enum.reduce modules, :not_found, fn
        module, :not_found -> h_mod_fun_arity(module, function, arity)
        _module, acc -> acc
      end

    unless result == :ok, do:
      IO.puts IO.ANSI.escape("%{red}No docs for #{function}/#{arity} have been found")

    :ok
  end

  def h(module, function, arity) when is_atom(module) and is_atom(function) and is_integer(arity) do
    case h_mod_fun_arity(module, function, arity) do
      :ok ->
        :ok
      :no_docs ->
        IO.puts IO.ANSI.escape("%{red}#{inspect module} was not compiled with docs")
      :not_found ->
        IO.puts IO.ANSI.escape("%{red}No docs for #{inspect module}.#{function}/#{arity} have been found")
    end

    :ok
  end

  def h(_, _, _) do
    IO.puts IO.ANSI.escape("%{red}Invalid arguments for h helper")
  end

  defp h_mod_fun_arity(mod, fun, arity) when is_atom(mod) and is_atom(fun) and is_integer(arity) do
    if docs = mod.__info__(:docs) do
      doc =
        cond do
          d = find_doc(docs, fun, arity)         -> d
          d = find_default_doc(docs, fun, arity) -> d
          true                                   -> nil
        end

      if doc do
        print_doc(doc)
        :ok
      else
        :not_found
      end
    else
      :no_docs
    end
  end

  defp find_doc(docs, function, arity) do
    if doc = List.keyfind(docs, { function, arity }, 0) do
      case elem(doc, 4) do
        false -> nil
        _ -> doc
      end
    end
  end

  defp find_default_doc(docs, function, min) do
    Enum.find docs, fn(doc) ->
      case elem(doc, 0) do
        { ^function, max } when max > min ->
          defaults = Enum.count elem(doc, 3), match?({ ://, _, _ }, &1)
          min + defaults >= max
        _ ->
          false
      end
    end
  end

  defp print_doc({ { fun, _ }, _line, kind, args, doc }) do
    args = Enum.map_join(args, ", ", print_doc_arg(&1))
    IO.puts IO.ANSI.escape("%{yellow}* #{kind} #{fun}(#{args})\n")
    IO.write IO.ANSI.escape_fragment("%{yellow}") <> doc <> IO.ANSI.escape_fragment("%{reset}")
  end

  defp print_doc_arg({ ://, _, [left, right] }) do
    print_doc_arg(left) <> " // " <> Macro.to_binary(right)
  end

  defp print_doc_arg({ var, _, _ }) do
    atom_to_binary(var)
  end

  @doc false
  def t(module) do
    types = lc type inlist Kernel.Typespec.beam_types(module), do: print_type(type)

    if types == [] do
      IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module} have been found")
    end

    :ok
  end

  @doc false
  def t(module, type) when is_atom(type) do
    types = lc {_, {t, _, _args}} = typespec inlist Kernel.Typespec.beam_types(module),
               t == type do
      print_type(typespec)
      typespec
    end

    if types == [] do
       IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module}.#{type} have been found")
    end

    :ok
  end

  @doc false
  def t(module, type, arity) do
    types = lc {_, {t, _, args}} = typespec inlist Kernel.Typespec.beam_types(module),
               length(args) == arity and t == type, do: typespec

    case types do
     [] ->
       IO.puts  IO.ANSI.escape("%{red}No types for #{inspect module}.#{type}/#{arity} have been found")
     [type] ->
       print_type(type)
    end

    :ok
  end

  @doc false
  def s(module) do
    specs = lc spec inlist beam_specs(module), do: print_spec(spec)

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module} have been found")
    end

    :ok
  end

  @doc false
  def s(module, function) when is_atom(function) do
    specs = lc {_kind, {{f, _arity}, _spec}} = spec inlist beam_specs(module),
               f == function do
      print_spec(spec)
      spec
    end

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  @doc false
  def s(module, function, arity) do
    specs = lc {_kind, {{f, a}, _spec}} = spec inlist beam_specs(module),
               f == function and a == arity do
      print_spec(spec)
      spec
    end

    if specs == [] do
      IO.puts  IO.ANSI.escape("%{red}No specs for #{inspect module}.#{function} have been found")
    end

    :ok
  end

  defp beam_specs(module) do
    specs = Enum.map(Kernel.Typespec.beam_specs(module), {:spec, &1})
    callbacks = Enum.map(Kernel.Typespec.beam_callbacks(module), {:callback, &1})
    List.concat(specs, callbacks)
  end

  defp print_type({ kind, type }) do
    ast = Kernel.Typespec.type_to_ast(type)
    IO.puts IO.ANSI.escape("%{yellow}@#{kind} #{Macro.to_binary(ast)}")
    true
  end

  defp print_spec({kind, { { name, _arity }, specs }}) do
    Enum.each specs, fn(spec) ->
      binary = Macro.to_binary Kernel.Typespec.spec_to_ast(name, spec)
      IO.puts IO.ANSI.escape("%{yellow}@#{kind} #{binary}")
    end
    true
  end
end


use new templates

use new templates

defmodule Core.EventMailer do
  use Phoenix.Swoosh,
    view: Core.EmailView,
    layout: {Core.EmailView, :email}

  require Logger
  import ShorterMaps

  def on_create(id, slug, event) do
    Logger.info "Sending email to Sam for event #{id}"

    new()
    |> to({"Sam Briggs", "sam@brandnewcongress.org"})
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({event.contact.name, "us@mail.brandnewcongress.org"})
    |> subject("New User Submitted Event!")
    |> render_body("new-event.text", %{id: id, slug: slug, event: event})
    |> Core.Mailer.deliver()
  end

  def failure_alert(body) do
    Logger.info "Sending email to Ben because of failure on Typeform webhook"

    {:ok, stringified} = Poison.encode(body)

    new()
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({"BNC Errors", "us@mail.brandnewcongress.org"})
    |> subject("Typeform Error")
    |> render_body("event-failure.text", %{raw: stringified})
    |> Core.Mailer.deliver()
  end

  def bad_event_alert(body) do
    Logger.info "Sending email to Ben because of bad event"

    {:ok, stringified} = Poison.encode(body)

    new()
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({"BNC Errors", "us@mail.brandnewcongress.org"})
    |> subject("Bad Event Error")
    |> render_body("event-failure.text", %{raw: stringified})
    |> Core.Mailer.deliver()
  end

  def on_rsvp(event, params = %{"first_name" => first_name, "last_name" => last_name, "email" => email}) do
    candidate =
      event.tags
      |> Enum.filter(&(String.contains?(&1, "Calendar: ")))
      |> Enum.map(&(&1 |> String.split(":") |> List.last() |> String.trim()))
      |> Enum.filter(&(not Enum.member?(["Brand New Congress", "Justice Democrats"], &1)))
      |> List.first()

    candidate = case candidate do
      nil -> "Justice Democrats"
      cand -> cand
    end

    event = Map.put(event, :rsvp_download_url, "https://admin.justicedemocrats.com/rsvps/#{Osdi.Event.rsvp_link_for(event.name)}")

    send_attendee_email(event, params, candidate)
    send_host_email(event, params, candidate)
  end

  defp send_attendee_email(event, params = %{"first_name" => first_name, "last_name" => last_name, "email" => email}, candidate) do
    Logger.info "Sending email to #{email} because they RSVPed to #{event.name}"
    params = ~M{first_name, last_name, email, candidate, event}

    new()
    |> to({"#{first_name} #{last_name}", email})
    |> from({event.contact.name || event.contact.email_address, "events@justicedemocrats.com"})
    |> subject("RSVP Confirmation: #{event.title}")
    |> render_body(:"rsvp-email", params)
    |> Core.Mailer.deliver()
  end

  defp send_host_email(event, params = %{"first_name" => first_name, "last_name" => last_name, "email" => email}, candidate) do
    Logger.info "Sending email to #{email} because someone RSVPed to their event, #{event.name}"
    params = ~M{first_name, last_name, email, candidate, event}

    new()
    |> to({"#{event.contact.name}", event.contact.email_address})
    |> from({"#{candidate} Events Team", "events@justicedemocrats.com"})
    |> subject("A New RSVP for #{event.title}!")
    |> render_body(:"rsvp-host-email", params)
    |> Core.Mailer.deliver()
  end
end

defmodule Core.EventMailer do
  use Phoenix.Swoosh,
    view: Core.EmailView,
    layout: {Core.EmailView, :email}

  require Logger
  import ShorterMaps

  def on_create(id, slug, event) do
    Logger.info "Sending email to Sam for event #{id}"

    new()
    |> to({"Sam Briggs", "sam@brandnewcongress.org"})
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({event.contact.name, "us@mail.brandnewcongress.org"})
    |> subject("New User Submitted Event!")
    |> render_body("new-event.text", %{id: id, slug: slug, event: event})
    |> Core.Mailer.deliver()
  end

  def failure_alert(body) do
    Logger.info "Sending email to Ben because of failure on Typeform webhook"

    {:ok, stringified} = Poison.encode(body)

    new()
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({"BNC Errors", "us@mail.brandnewcongress.org"})
    |> subject("Typeform Error")
    |> render_body("event-failure.text", %{raw: stringified})
    |> Core.Mailer.deliver()
  end

  def bad_event_alert(body) do
    Logger.info "Sending email to Ben because of bad event"

    {:ok, stringified} = Poison.encode(body)

    new()
    |> to({"Ben Packer", "ben@brandnewcongress.org"})
    |> from({"BNC Errors", "us@mail.brandnewcongress.org"})
    |> subject("Bad Event Error")
    |> render_body("event-failure.text", %{raw: stringified})
    |> Core.Mailer.deliver()
  end

  def on_rsvp(event, %{"first_name" => first_name, "last_name" => last_name, "email" => email}) do
    Logger.info "Sending email to #{email} because of RSVP to #{event.name}"

    candidate =
      event.tags
      |> Enum.filter(&(String.contains?(&1, "Calendar: ")))
      |> Enum.map(&(&1 |> String.split(":") |> List.last() |> String.trim()))
      |> Enum.filter(&(not Enum.member?(["Brand New Congress", "Justice Democrats"], &1)))
      |> List.first()

    candidate = case candidate do
      nil -> "Justice Democrats"
      cand -> cand
    end

    params = ~M{first_name, last_name, email, candidate, event}

    new()
    |> to({"#{first_name} #{last_name}", email})
    |> from({event.contact.name || event.contact.email_address, "events@brandnewcongress.org"})
    |> subject("RSVP Confirmation: #{event.title}")
    |> render_body(:"rsvp-email", params)
    |> Core.Mailer.deliver()
  end
end


Fix compiler warning for grade-test's add/3 spec

    school.exs:11: warning: string() type use is discouraged. For character lists, use char_list() type, for strings, String.t()
        school.exs:11: School (module)

defmodule School do
  @moduledoc """
  Simulate students in a school.

  Each student is in a grade.
  """

  @doc """
  Add a student to a particular grade in school.
  """
  @spec add(Dict.t, String.t, pos_integer) :: Dict.t
  def add(db, name, grade) do

  end

  @doc """
  Return the names of the students in a particular grade.
  """
  @spec grade(Dict.t, pos_integer) :: [String]
  def grade(db, grade) do

  end

  @doc """
  Sorts the school by grade and name.
  """
  @spec sort(Dict) :: Dict.t
  def sort(db) do

  end
end

defmodule School do
  @moduledoc """
  Simulate students in a school.

  Each student is in a grade.
  """

  @doc """
  Add a student to a particular grade in school.
  """
  @spec add(Dict.t, string, pos_integer) :: Dict.t
  def add(db, name, grade) do

  end

  @doc """
  Return the names of the students in a particular grade.
  """
  @spec grade(Dict.t, pos_integer) :: [String]
  def grade(db, grade) do

  end

  @doc """
  Sorts the school by grade and name.
  """
  @spec sort(Dict) :: Dict.t
  def sort(db) do

  end
end