Hello again! Today we're going to work a bit more on BEAM Toolbox, specifically on migrating the data layer to use Maps. Let's get started!

Project

I pushed a branch called before_episode_57 that consists of the code I'm working with. The only change from what we did in episode 53 is I've upgraded Calliope, and consequently had to unescape a lot of the embedded partials since Calliope now assumes unsafe data unless you tell it otherwise.

Migrating the data layer to Maps

Here, I'm just going to rather quickly replace all of the unstructured data in my app with a hierarchy of structs. We'll first write out what it looks like to build the data. Open up lib/beam_toolbox/data.ex:

defmodule BeamToolbox.Data do
  alias BeamToolbox.Models.CategoryGroup
  alias BeamToolbox.Models.Category
  alias BeamToolbox.Models.Project

  ## FIXTURES / DUMMY DATA
  def category_groups do
    [
      %CategoryGroup{name: "Testing", categories:
        [
          %Category{name: "Integration Testing", projects:
            [
              %Project{name: "Amrita", website: "http://amrita.io", github: "http://github.com/josephwilk/amrita"},
              %Project{name: "Hound", website: "https://github.com/HashNuke/hound", github: "https://github.com/HashNuke/hound"}
            ]
          },
          %Category{name: "General Testing", projects:
            [
              %Project{name: "Common Test", website: "http://www.erlang.org/doc/apps/common_test/basics_chapter.html", github: ""},
              %Project{name: "EUnit", website: "http://www.erlang.org/doc/apps/eunit/chapter.html", github: ""},
              %Project{name: "Triq", website: "https://github.com/krestenkrab/triq", github: "https://github.com/krestenkrab/triq"},
              %Project{name: "test_server", website: "http://www.erlang.org/doc/man/test_server.html", github: ""},
              %Project{name: "tsung", website: "http://tsung.erlang-projects.org/", github: "https://github.com/processone/tsung"}
            ]
          }
        ]
      },
      %CategoryGroup{name: "Development Tools", categories:
        [
          %Category{name: "Code Reloading", projects:
            [
              %Project{name: "sync", website: "https://github.com/rustyio/sync", github: "https://github.com/rustyio/sync"},
              %Project{name: "active", website: "https://github.com/proger/active", github: "https://github.com/proger/active"}
            ]
          },
          %Category{name: "File System Monitoring", projects:
            [
              %Project{name: "erlfsmon", website: "https://github.com/proger/erlfsmon", github: "https://github.com/proger/erlfsmon"}
            ]
          }
        ]
      },
      %CategoryGroup{name: "HTML and Markup", projects:
        []
      }
    ]
  end
end

Next, let's just convert those models to use structs. First, open up lib/beam_toolbox/models/category_group.ex:

defmodule BeamToolbox.Models.CategoryGroup do
  defstruct [name: nil, categories: []]
  use BeamToolbox.Model
end

Next, let's take care of Category:

defmodule BeamToolbox.Models.Category do
  defstruct [name: nil, projects: []]
  use BeamToolbox.Model
end

Finally, let's open up Project:

defmodule BeamToolbox.Models.Project do
  defstruct [:name, :website, :github]
  use BeamToolbox.Model
end

Now, our BeamToolbox.Model needs to change now that we're using structs. Let's take care of that:

defmodule BeamToolbox.Model do
  defmacro __using__(opts) do
    quote do
      def list(data) do
        list(data, [])
      end

      def find(data, expected_name) do
        Enum.find(data, fn(data) -> data.name == expected_name end)
      end

      defp list([data|rest], acc) do
        list(rest, [data.name|acc])
      end
      defp list([], acc) do
        Enum.reverse(acc)
      end
    end
  end
end

Alright, so now let's see if we can get the CategoryGroupTest working. Open it up:

defmodule BeamToolbox.Models.CategoryGroupTest do
  use ExUnit.Case
  alias BeamToolbox.Models.CategoryGroup

  # We'll need to change the test data to use structs...that's all
  @test_data [
    %CategoryGroup{name: "Testing", categories: []},
    %CategoryGroup{name: "Development Tools", categories: []},
    %CategoryGroup{name: "HTML and Markup", categories: []}
  ]

  test "Listing CategoryGroups" do
    assert ["Testing", "Development Tools", "HTML and Markup"] == @test_data |> CategoryGroup.list
  end

  test "Finding a CategoryGroup by name" do
    assert Enum.at(@test_data, 1) == @test_data |> CategoryGroup.find("Development Tools")
  end
end

So that was a trivial change. Let's run it:

mix test lib/beam_toolbox/models/category_group_test.exs

Alright, they pass. Let's very quickly do the same thing to both the category and product tests:

defmodule BeamToolbox.Models.CategoryTest do
  #...
  @test_data [
    %Category{name: "Code Reloading", projects: []},
    %Category{name: "File System Monitoring", projects: []}
  ]
  #...
end
defmodule BeamToolbox.Models.ProjectTest do
  #...
  @test_data [
    %Project{name: "sync", website: "https://github.com/rustyio/sync", github: "https://github.com/rustyio/sync"},
    %Project{name: "active", website: "https://github.com/proger/active", github: "https://github.com/proger/active"}
  ]
  #...
end

Now let's run the whole suite....

mix test

Alright, so there's a single test still failing. Let's open it up:

defmodule BeamToolbox.DataTest do
  # ...
  # First, let's change how we determine that it's listing category groups
  test "it can list category_groups" do
    assert hd(Data.category_groups).__struct__ == CategoryGroup
  end
  # Next, we need to alias this so we can write it short like above:
  alias BeamToolbox.Models.CategoryGroup
end

Alright, run the tests:

mix test

Summary

And they pass. That's it, that's all it took to migrate a project to use maps, and we were able to simplify a fair bit of the code. See you soon!