Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Phoenix & Ember Living Together - Jacksonville 11/10/2015

Phoenix & Ember Living Together - Jacksonville 11/10/2015

How DockYard is working with Phoenix and Ember

Brian Cardarella

November 12, 2015
Tweet

More Decks by Brian Cardarella

Other Decks in Programming

Transcript

  1. priv !"" static #"" css $ #"" app.css $ !""

    app.css.map #"" images $ #"" favicon.ico $ !"" phoenix.png #"" js $ #"" app.js $ !"" app.js.map !"" robots.txt
  2. priv !"" static #"" css $ #"" app.css $ !""

    app.css.map #"" images $ #"" favicon.ico $ !"" phoenix.png #"" js $ #"" app.js $ !"" app.js.map !"" robots.txt
  3. * standardized schema for JSON based APIs * is very

    verbose * requires special MIME type
  4. defmodule MyApp.Router do pipeline :api do accepts, [“json-api”] end scope

    “/api” MyApp do pipe_through :api resources “/accounts”, AccountsController end end
  5. Copied from plug’s documentation # https://github.com/elixir-lang/plug/tree/master/lib/plug/mime.ex Maps MIME types to

    file extensions and vice versa. MIME types can be extended in your application configuration as follows: config :plug, :mimes, %{ "application/vnd.api+json" => ["json-api"] } After adding the configuration, Plug needs to be recompiled. If you are using mix, it can be done with: $ touch deps/plug/mix.exs $ mix deps.compile plug
  6. Copied from plug’s documentation # config/config.exs config :plug, :mimes, %{

    "application/vnd.api+json" => ["json-api"] } Add the MIME type to your Phoenix config
  7. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  8. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  9. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  10. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  11. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  12. defmodule MyApp.Deserialize do def init(options) do options end def call(%Plug.Conn{method:

    "POST"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PUT"}=conn, _opts) do _deserialize(conn) end def call(%Plug.Conn{method: "PATCH"}=conn, _opts) do _deserialize(conn) end def call(conn, _opts), do: conn defp _deserialize(%Plug.Conn{}=conn) do Map.put(conn, :params, _deserialize(conn.params)) end defp _deserialize(%{}=params) do Enum.into(params, %{}, fn({key, value}) -> { _underscore(key), _deserialize(value) } end) end defp _deserialize(value), do: value defp _underscore(key), do: String.replace(key, "-", "_") end
  13. defmodule MyApp.Router do pipeline :api do accepts, [“json-api”] end scope

    “/api” MyApp do pipe_through :api resources “/accounts”, AccountsController end end
  14. defmodule MyApp.Router do pipeline :api do accepts, [“json-api”] plug MyApp.Deserialize

    end scope “/api” MyApp do pipe_through :api resources “/accounts”, AccountsController end end
  15. %{ “data" => %{ “type" => "authors", “attributes" => %{

    “first_name” => "Brian", “last_name” => "Cardarella" } } }
  16. %{ “data" => %{ “type" => "authors", “attributes" => %{

    “first_name” => "Brian", “last_name” => "Cardarella" } } }
  17. %{ “data" => %{ “type" => "authors", “attributes" => %{

    “first_name” => "Brian", “last_name” => "Cardarella" } } }
  18. %{ “data" => %{ “type" => "authors", “attributes" => %{

    “first_name” => "Brian", “last_name” => "Cardarella" } } }
  19. Actions def create(conn, %{“data” => %{“attributes” => data, “type” =>

    “authors”}}) def post(conn, %{“data” => %{“attributes” => data, “type” => “authors”}}) def put(conn, %{“data” => %{“attributes” => data, “type” => “authors”}})
  20. Actions def create(conn, %{“data” => %{“attributes” => data, “type” =>

    “authors”}}) def post(conn, %{“data” => %{“attributes” => data, “type” => “authors”}}) def put(conn, %{“data” => %{“attributes” => data, “type” => “authors”}})
  21. Actions def create(conn, %{“data” => %{“attributes” => data, “type” =>

    “authors”}}) def post(conn, %{“data” => %{“attributes” => data, “type” => “authors”}}) def put(conn, %{“data” => %{“attributes” => data, “type” => “authors”}}) def delete(conn, %{“id” => id}) def show(conn, %{“id” => id}) def index(conn, _params)
  22. View defmodule MyApp.AuthorsView do use PhoenixExample.Web, :view use JaSerializer.PhoenixView attributes

    [:first_name, :last_name] end web/views/authors_view.ex https://github.com/AgilionApps/ja_serializer
  23. API Unit Tests test "`create` insert into the database and

    return payload" do count = MyApp.Repo.all(MyApp.Author) |> length post(conn, authors_path(conn, :create), json_for(:author, @author)) |> json_response(201) |> assert_payload_contains(%{ "authors": %{attributes: %{first-name: "Gizmo"}} }) assert count + 1 == MyApp.Repo.all(MyApp.Author) |> length end
  24. API Unit Tests test "`create` insert into the database and

    return payload" do count = MyApp.Repo.all(MyApp.Author) |> length post(conn, authors_path(conn, :create), json_for(:author, @author)) |> json_response(201) |> assert_payload_contains(%{ "authors": %{attributes: %{first-name: "Gizmo"}} }) assert count + 1 == MyApp.Repo.all(MyApp.Author) |> length end
  25. %{ “data" => %{ “type" => "authors", “attributes" => %{

    “first_name” => "Brian", “last_name” => "Cardarella" } } }
  26. def json_for(type, attributes) do %{ "data" => %{ "type" =>

    Atom.to_string(type) |> String.replace("_", "-"), "attributes" => attributes }, "format" => "json-api" } end def json_for(type, attributes, id) do json = json_for(type, attributes) put_in json["data"]["id"], id end
  27. def json_for(type, attributes) do %{ "data" => %{ "type" =>

    Atom.to_string(type) |> String.replace("_", "-"), "attributes" => attributes }, "format" => "json-api" } end def json_for(type, attributes, id) do json = json_for(type, attributes) put_in json["data"]["id"], id end
  28. API Unit Tests test "`create` insert into the database and

    return payload" do count = MyApp.Repo.all(MyApp.Author) |> length post(conn, authors_path(conn, :create), json_for(:author, @author)) |> json_response(201) |> assert_payload_contains(%{ "authors": %{attributes: %{first-name: "Gizmo"}} }) assert count + 1 == MyApp.Repo.all(MyApp.Author) |> length end https://github.com/danmcclain/voorhees
  29. API Unit Tests test "`show` an author" do %{authors: %{one:

    author}} = fixtures(:authors) get(conn, authors_path(conn, :show, author.id)) |> json_response(200) |> assert_payload_contains(%{ "authors": %{attributes: %{first-name: "Brian"}} }) end
  30. API Unit Tests test "`show` an author" do %{authors: %{one:

    author}} = fixtures(:authors) get(conn, authors_path(conn, :show, author.id)) |> json_response(200) |> assert_payload_contains(%{ "authors": %{attributes: %{first-name: "Brian"}} }) end https://github.com/dockyard/ecto_fixtures
  31. authors model: MyApp.Author, repo: MyApp.Repo do valid do first_name "George"

    last_name "Washington" email "[email protected]" date_of_birth Ecto.Date.from_erl({1990,1,1}) password_hash Comeonin.Bcrypt.hashpwsalt("password") end one inherits: valid do id 1 email "[email protected]" first_name "Leroy" last_name "Jenkins" end end test/fixtures/authors.exs
  32. Setting Up Data Per Test if Mix.env == :test do

    post "/fixtures/start", FixturesController, :start post "/fixtures", FixturesController, :create end
  33. Setting Up Data Per Test defmodule MyApp.FixturesController do use MyApp.Web,

    :controller import EctoFixtures, only: [fixtures: 1] def start(conn, _params) do Ecto.Adapters.SQL.begin_test_transaction(MyApp.Repo) send_resp(conn, 200, "") end def create(conn, %{"fixtures" => fixtures}) do Ecto.Adapters.SQL.restart_test_transaction(MyApp.Repo) Poison.decode!(fixtures) |> Enum.each(fn(name) -> fixtures(name) end) send_resp(conn, 200, "") end end
  34. Setting Up Data Per Test defmodule MyApp.FixturesController do use MyApp.Web,

    :controller import EctoFixtures, only: [fixtures: 1] def start(conn, _params) do Ecto.Adapters.SQL.begin_test_transaction(MyApp.Repo) send_resp(conn, 200, "") end def create(conn, %{"fixtures" => fixtures}) do Ecto.Adapters.SQL.restart_test_transaction(MyApp.Repo) Poison.decode!(fixtures) |> Enum.each(fn(name) -> fixtures(name) end) send_resp(conn, 200, "") end end
  35. Setting Up Data Per Test defmodule MyApp.FixturesController do use MyApp.Web,

    :controller import EctoFixtures, only: [fixtures: 1] def start(conn, _params) do Ecto.Adapters.SQL.begin_test_transaction(MyApp.Repo) send_resp(conn, 200, "") end def create(conn, %{"fixtures" => fixtures}) do Ecto.Adapters.SQL.restart_test_transaction(MyApp.Repo) Poison.decode!(fixtures) |> Enum.each(fn(name) -> fixtures(name) end) send_resp(conn, 200, "") end end
  36. Setting Up Data Per Test defmodule MyApp.FixturesController do use MyApp.Web,

    :controller import EctoFixtures, only: [fixtures: 1] def start(conn, _params) do Ecto.Adapters.SQL.begin_test_transaction(MyApp.Repo) send_resp(conn, 200, "") end def create(conn, %{"fixtures" => fixtures}) do Ecto.Adapters.SQL.restart_test_transaction(MyApp.Repo) Poison.decode!(fixtures) |> Enum.each(fn(name) -> fixtures(name) end) send_resp(conn, 200, "") end end
  37. Setting Up Data Per Test defmodule MyApp.FixturesController do use MyApp.Web,

    :controller import EctoFixtures, only: [fixtures: 1] def start(conn, _params) do Ecto.Adapters.SQL.begin_test_transaction(MyApp.Repo) send_resp(conn, 200, "") end def create(conn, %{"fixtures" => fixtures}) do Ecto.Adapters.SQL.restart_test_transaction(MyApp.Repo) Poison.decode!(fixtures) |> Enum.each(fn(name) -> fixtures(name) end) send_resp(conn, 200, "") end end test/support/fixtures_controller.ex
  38. Calling the fixtures in your Ember Test Suite module(“my awesome

    test module”, { setup() { Ember.$.post(“api/fixtures/start”, { async: false }); } });
  39. Calling the fixtures in your Ember Test Suite module(“my awesome

    test module”, { setup() { Ember.$.post(“api/fixtures/start”, { async: false }); } });
  40. Calling the fixtures in your Ember Test Suite test(“my awesome

    test”, function(assert) { Ember.$.post(“api/fixtures”, { data: { fixtures: [“foo”, “bar”] }, async: false }); });
  41. Calling the fixtures in your Ember Test Suite test(“my awesome

    test”, function(assert) { Ember.$.post(“api/fixtures”, { data: { fixtures: [“foo”, “bar”] }, async: false }); });
  42. Calling the fixtures in your Ember Test Suite test(“my awesome

    test”, function(assert) { Ember.$.post(“api/fixtures”, { data: { fixtures: [“foo”, “bar”] }, async: false }); });
  43. Other Topics Not Covered * Running with CI * Speeding

    up test suite * Deployment * Testing Email via Ember (that I will briefly cover right now)