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

Phoenix & Ember Living Together - Jacksonville ...

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

How DockYard is working with Phoenix and Ember

Avatar for Brian Cardarella

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)