1

I've created a JSON api using Elixir and the Phoenix

I have an endpoint for a create action in my controller that takes json data which looks like this:

     [{"opens_detail"=>
          [{"ua"=>"Linux/Ubuntu/Chrome/Chrome 28.0.1500.53",
              "ip"=>"55.55.55.55",
              "ts"=>1365190001,
              "location"=>"Georgia, US"}],
         "template"=>"example-template",
         "metadata"=>{"user_id"=>"123", "website"=>"www.example.com"},
         "clicks"=>42,
         "ts"=>1365190000,
         "state"=>"sent",
         "clicks_detail"=>
          [{"ua"=>"Linux/Ubuntu/Chrome/Chrome 28.0.1500.53",
              "ip"=>"55.55.55.55",
              "ts"=>1365190001,
              "url"=>"http://www.example.com",
              "location"=>"Georgia, US"}],
         "email"=>"recipient.email@example.com",
         "subject"=>"example subject",
         "sender"=>"sender@example.com",
         "_id"=>"abc123abc123abc123abc123",
         "tags"=>["password-reset"],
         "opens"=>42}]

My goal is to take this json and create a new one from it where some keys and values are renamed to match my schema below:

in web/models/messages.ex

   ...
      schema "messages" do
        field :sender, :string
        field :uniq_id, :string # equal to '_id' in the payload
        field :ts, :utc_datetime
        field :template, :string
        field :subject, :string
        field :email, :string
        field :tags, {:array, :string}
        field :opens, :integer
        field :opens_ip, :string # equal to nested 'ip' value in 'open_details'
        field :opens_location, :string # equal to nested 'location' value in 'open_details'
        field :clicks, :integer
        field :clicks_ip, :string # equal to nested 'ip' value in 'click_details'
        field :clicks_location, :string # equal to nested 'location' value in 'click_details'
        field :status, :string # equal to the "state" in the payload

        timestamps()
      end
  ...

This is what I tried:

in web/controller/message_controller.ex:

  def create(conn, payload) do

    %{ payload |
      "uniq_id" => payload["_id"],
      "status" => payload["type"]
      "open_ips" =>  Enum.at(payload["opens_detail"], 0)['ip'],
      "open_location" => Enum.at(payload["opens_detail"], 0)['location'],
      "click_ips" =>  Enum.at(payload["clicks_detail"], 0)['ip'],
      "click_location" => Enum.at(payload["clicks_detail"], 0)['location'],
    }

    changeset = Message.changeset(%Message{}, payload)

   ...

  end

but it quickly became clear that it wouldn't work also because I would still need to remove some keys.

I'm coming from Ruby/Python (Rails/Django) and don't want to start polluting my learning of functional programming, specifically elixir/phoenix, with my OO knowledge.

How would you solve this problem?

Cyzanfar
  • 6,997
  • 9
  • 43
  • 81

1 Answers1

2

How would you solve this problem?

I would create a new map from scratch instead of updating the original map. You can use get_in to simplify the logic to access nested fields. Here's an example:

map = %{
  uniq_id:        get_in(payload, ["_id"]),
  open_ips:       get_in(payload, ["opens_detail", Access.at(0), "ip"]),
  open_locations: get_in(payload, ["opens_detail", Access.at(0), "location"]),
}

If you want to pick a subset of fields from the original map, you can use Map.merge and Map.take:

Map.merge(Map.take(payload, [:sender, ...]), %{uniq_id: ...})

But if it's only a couple of fields I'd rather write them out manually.

Dogbert
  • 212,659
  • 41
  • 396
  • 397
  • very nice thanks a lot. As for the remaining key/values i'd need to redefine them in the map like so: `%{ ..., sender: get_in(payload, ["sender"]), ...}` ? – Cyzanfar Feb 09 '18 at 17:19
  • 1
    Yep, I'd do it that way. You can also pick some keys from the original map like this as well: `Map.merge(Map.take(payload, [:sender, ...]), %{uniq_id: ...})` but if it's only a couple of fields I'd rather write them out manually. – Dogbert Feb 09 '18 at 17:25