Save 37% off PRO during our Black Friday Sale! »

Making Invalid States Invalid in LiveView

Making Invalid States Invalid in LiveView

Have you ever found a bug in your LiveView code because your app somehow had invalid and incoherent state?

“Your total is -$500.”
“Your return flight is .”
“The temperature is -600°F.”

When programming user interfaces, it’s important that we do not display inconsistent states to our users. But how can we prevent those bugs?

By making those invalid states impossible to represent!

Join me as we walk through a LiveView example that has bugs because of invalid states. As we fix them, you’ll learn to model domains in a way that makes rendering invalid states impossible. Your users and your future self will thank you.

A19876f5694283f26a061746ba82c3b0?s=128

German Velasco

October 15, 2021
Tweet

Transcript

  1. MAKING INVALID STATES INVALID in LiveView

  2. MAKING INVALID STATES INVALID in LiveView AN EXPLORATION

  3. WHAT DOES THAT MEAN?

  4. How we model our domain constrains what states are allowed

  5. Model our domain to remove states we don't want

  6. NOT NEW

  7. NOT NEW ▸ OCaml

  8. NOT NEW ▸ OCaml ▸ Elm

  9. NOT NEW ▸ OCaml ▸ Elm ▸ F#

  10. OTHER NAMES

  11. OTHER NAMES ▸ Making invalid states unrepresentable

  12. OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal

    states unrepresentable
  13. OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal

    states unrepresentable ▸ Making impossible states impossible
  14. BRINGING IT TO LiveView

  15. BRINGING IT TO LiveView ▸ Persistent state

  16. BRINGING IT TO LiveView ▸ Persistent state ▸ That our

    users see
  17. A FLIGHT BOOKER EXAMPLE !

  18. None
  19. FIRST IMPLEMENTATION

  20. ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket

    |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end
  21. ASSIGNING VALUES def mount(_, _, socket) do { :ok, socket

    |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) } end
  22. RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",

    "two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form>
  23. RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",

    "two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form>
  24. RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",

    "two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form>
  25. RENDERING THE ASSIGNS <form phx-submit="book"> <%= select :booking, :flight_type, ["one-way",

    "two-way"], value: @direction %> <%= text_input :booking, :departure, value: @departure %> <%= text_input :booking, :return, value: @return %> <%= submit "Book" %> </form>
  26. HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do

    {:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end
  27. HANDLING THE EVENT def handle_event("book", %{"booking" => params}, socket) do

    {:ok, message} = FlightBooker.book_trip(params) socket |> put_flash(:info, message) |> noreply() end
  28. BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"

    => "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end
  29. BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"

    => "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end
  30. BOOKING THE TRIP def book_trip(params) do case params do %{"flight_type"

    => "one-way", "departure" => departure} -> {:ok, one_way_message(departure)} %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> {:ok, two_way_message(departure, return)} end end
  31. None
  32. ! THE BUGS

  33. ! THE BUGS

  34. ! THE BUGS ▸ ! One-way flight that got charged

    for a return flight
  35. ! THE BUGS ▸ ! One-way flight that got charged

    for a return flight ▸ " Two-way flight without a return flight
  36. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  37. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  38. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  39. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  40. OUR CURRENT (IMPLICIT) DOMAIN MODEL %{ direction: "one-way" | "two-way",

    departure: Date.t() | nil, return: Date.t() | nil }
  41. POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}

    %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  42. POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}

    %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  43. POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}

    %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  44. INVALID STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}

    %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  45. VALID STATES # one-way %{direction: "one-way", departure: Date.t(), return: Date.t()}

    %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  46. ONLY 2/8 STATES ARE VALID! # one-way %{direction: "one-way", departure:

    Date.t(), return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()}
  47. A MORE constrained FLIGHT BOOKER DOMAIN MODEL

  48. A MORE constrained FLIGHT BOOKER DOMAIN MODEL

  49. A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight

    with departure date
  50. A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight

    with departure date ▸ OR
  51. A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight

    with departure date ▸ OR ▸ Two-way flight with departure and return dates
  52. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  53. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  54. A MORE constrained FLIGHT BOOKER DOMAIN MODEL @type flight_booker ::

    {:one_way, Date.t()} | {:two_way, Date.t(), Date.t()}
  55. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    {:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end
  56. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    {:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end
  57. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    {:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end
  58. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    %OneWay{departure: date} end def two_way(%Date{} = departure, %Date{} = return) do %TwoWay{departure: departure, return: return} end end
  59. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    %OneWay{departure: date} end def two_way(%Date{} = departure, %Date{} = return) do %TwoWay{departure: departure, return: return} end end
  60. TAGGED VALUES defmodule FlightBooker do def one_way(%Date{} = date) do

    {:one_way, date} end def two_way(%Date{} = departure, %Date{} = return) do {:two_way, departure, return} end end
  61. IMPLICATIONS OF OUR NEW DOMAIN MODEL

  62. IMPLICATIONS OF OUR NEW DOMAIN MODEL ▸ Render complex types

  63. IMPLICATIONS OF OUR NEW DOMAIN MODEL ▸ Render complex types

    ▸ Handle events with user input
  64. 1. RENDER COMPLEX TYPE def mount(_, _, socket) do today

    = Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end
  65. 1. RENDER COMPLEX TYPE def mount(_, _, socket) do today

    = Date.utc_today() booker = FlightBooker.one_way(today) {:ok, assign(socket, :booker, booker)} end
  66. 1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker

    do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  67. 1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker

    do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  68. 1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker

    do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  69. 1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker

    do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  70. 1. RENDER COMPLEX TYPE <form phx-submit="book" phx-change="update"> <%= case @booker

    do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  71. FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">

    <%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  72. FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">

    <%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, disabled: true %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  73. FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <form phx-submit="book" phx-change="update">

    <%= case @booker do %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <%= text_input :booking, :departure, value: departure %> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <%= text_input :booking, :departure, value: departure %> <%= text_input :booking, :return, value: return %> <% end %> <%= submit "Book" %> </form>
  74. INVALID STATE REMOVED %{ direction: "one-way", departure: Date.t(), return: Date.t()

    }
  75. 2. HANDLE EVENTS

  76. TRANSFORM USER INPUT THROUGH PARSING

  77. TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>

    more structured data
  78. TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>

    more structured data ▸ raw data |> valid domain
  79. TRANSFORM USER INPUT THROUGH PARSING ▸ less structured data |>

    more structured data ▸ raw data |> valid domain ▸ ⚠ |>
  80. HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,

    message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end
  81. HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,

    message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end
  82. HANDLE EVENT def handle_event("book", %{"booking" => params}, socket) do {:ok,

    message} = params |> parse_params_into_booking() |> FlightBooker.book_trip() socket |> put_flash(:info, message) |> noreply() end
  83. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  84. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  85. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  86. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  87. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  88. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  89. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  90. INVALID STATE REMOVED %{ direction: "two-way", departure: Date.t(), return: nil

    }
  91. defp parse_params_into_booking(params) do case params do %{"flight_type" => "one-way", "departure"

    => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  92. INVALID STATES REMOVED %{direction: "one-way", departure: nil, return: nil} %{direction:

    "one-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: nil, return: nil} %{direction: "two-way", departure: nil, return: Date.t()}
  93. Without any error handling, we have removed 6 invalid states!

  94. WHAT ABOUT ERROR HANDLING?

  95. WHAT ABOUT ERROR HANDLING? ▸ Validate and present errors

  96. WHAT ABOUT ERROR HANDLING? ▸ Validate and present errors ▸

    Potential for same problems
  97. POSSIBLE ERRORS @type error_fields :: :departure | :return @type errors

    :: [error_fields]
  98. ADDING ERRORS ASSIGN socket |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today())

    |> assign(:return, nil) + |> assign(:errors, [])
  99. (PREVIOUS) POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return:

    Date.t()} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  100. (PREVIOUS) POSSIBLE STATES # one-way %{direction: "one-way", departure: Date.t(), return:

    Date.t()} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  101. # one-way %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: []}

    %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t()} %{direction: "one-way", departure: Date.t(), return: nil} %{direction: "one-way", departure: nil, return: nil} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t()} %{direction: "two-way", departure: nil, return: Date.t()} %{direction: "two-way", departure: Date.t(), return: nil} %{direction: "two-way", departure: nil, return: nil}
  102. 8 X 4 = 32 POSSIBLE STATES! # one-way %{direction:

    "one-way", departure: Date.t(), return: Date.t(), errors: []} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: []} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: []} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: nil, errors: []} %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} %{direction: "one-way", departure: nil, return: nil, errors: [:return]} %{direction: "one-way", departure: nil, return: nil, errors: [:departure, :return]} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: []} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: []} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: nil, errors: []} %{direction: "two-way", departure: nil, return: nil, errors: [:departure]} %{direction: "two-way", departure: nil, return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  103. "Please select a valid return date" %{ direction: "one-way", departure:

    Date.t(), return: nil, errors: [:return] }
  104. "Please select a valid return date" %{ direction: "one-way", departure:

    Date.t(), return: nil, errors: [:return] } ▸ !
  105. ONLY 6 VALID STATES! ## one-way flight # one way

    flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  106. ONLY 6 VALID STATES! ## one-way flight # one way

    flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  107. ONLY 6 VALID STATES! ## one-way flight # one way

    flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  108. ONLY 6 VALID STATES! ## one-way flight # one way

    flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  109. ONLY 6 VALID STATES! ## one-way flight # one way

    flight %{direction: "one-way", departure: Date.t(), return: nil, errors: []} # one way flight with departure error %{direction: "one-way", departure: nil, return: nil, errors: [:departure]} ## two-way flight # two-way flight %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: []} # two-way flight with errors %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "two-way", departure: nil, return: nil, errors: [:departure, :return]}
  110. INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()

    | {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error}
  111. INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()

    | {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error}
  112. INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()

    | {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error}
  113. INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()

    | {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error}
  114. INCLUDING ERRORS IN OUR DOMAIN MODEL @type date_or_error :: Date.t()

    | {:error, any()} @type flight_booker :: {:one_way, date_or_error} | {:two_way, date_or_error, date_or_error}
  115. POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,

    Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}}
  116. POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,

    Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}}
  117. POSSIBLE STATES IN OUR DOMAIN MODEL # one way {:one_way,

    Date.t()} {:one_way, {:error, any()}} # two way {:two_way, departure: Date.t(), return: Date.t()} {:two_way, departure: {:error, any()}, return: Date.t()} {:two_way, departure: Date.t(), return: {:error, any()}} {:two_way, departure: {:error, any()}, return: {:error, any()}}
  118. RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do

    %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form>
  119. RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do

    %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form>
  120. RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do

    %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form>
  121. RENDERING COMPLEX TYPES <form phx-submit="book" phx-change="update"> <%= case @booker do

    %> <% {:one_way, departure} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "one-way" %> <.departure_input departure={departure} /> <% {:two_way, departure, return} -> %> <%= select :booking, :flight_type, ["one-way", "two-way"], value: "two-way" %> <.departure_input departure={departure} /> <.return_input return={return} /> <% end %> <%= submit "Book" %> </form>
  122. RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=

    case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end
  123. RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=

    case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end
  124. RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=

    case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end
  125. RENDERING COMPLEX TYPES (DEPARTURE INPUT) def departure_input(assigns) do ~H""" <%=

    case @departure do %> <% {:error, value} -> %> <%= text_input :booking, :departure, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :departure, value: value %> <% end %> """ end
  126. RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=

    case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end
  127. RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=

    case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end
  128. RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=

    case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end
  129. RENDERING COMPLEX TYPES (RETURN INPUT) def return_input(assigns) do ~H""" <%=

    case @return do %> <% {:error, value} -> %> <%= text_input :booking, :return, value: value, class: "invalid" %> <span class="invalid-feedback">is invalid</span> <% value -> %> <%= text_input :booking, :return, value: value %> <% end %> """ end
  130. TRANSFORM USER INPUT (BEFORE) defp parse_params_into_booking(params) do case params do

    %{"flight_type" => "one-way", "departure" => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  131. TRANSFORM USER INPUT (BEFORE) defp parse_params_into_booking(params) do case params do

    %{"flight_type" => "one-way", "departure" => departure} -> departure |> Date.from_iso8601!() |> FlightBooker.one_way() %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> departure_date = departure |> Date.from_iso8601!() return_date = return |> Date.from_iso8601!() FlightBooker.two_way(departure_date, return_date) %{"flight_type" => "two-way", "departure" => departure} -> departure_date = departure |> Date.from_iso8601!() return_date = Date.utc_today() FlightBooker.two_way(departure_date, return_date) end end
  132. TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do

    %{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end
  133. TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do

    %{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end
  134. TRANSFORM USER INPUT (NOW) defp parse_params_into_booking(params) do case params do

    %{"flight_type" => "one-way", "departure" => departure} -> FlightBooker.one_way(departure) %{"flight_type" => "two-way", "departure" => departure, "return" => return} -> FlightBooker.two_way(departure, return) %{"flight_type" => "two-way", "departure" => departure} -> return = Date.utc_today() |> Date.to_string() FlightBooker.two_way(departure, return) end end
  135. PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>

    parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  136. PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>

    parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  137. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  138. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  139. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  140. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  141. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  142. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  143. PARSING INPUT INTO OUR DOMAIN defp parse_date(%Date{} = date), do:

    date defp parse_date(string_date) when is_binary(string_date) do case Date.from_iso8601(string_date) do {:ok, date} -> date {:error, _} -> error(string_date) end end defp parse_date(value), do: error(value) defp error(value), do: {:error, value}
  144. PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>

    parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  145. PARSING INPUT INTO OUR DOMAIN def one_way(departure_value) do departure_value |>

    parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  146. PARSING INPUT INTO OUR DOMAIN defp build_one_way(date) do {:one_way, date}

    end defp build_two_way(departure, return) do {:two_way, departure, return} end
  147. 26 INVALID STATES REMOVED # one-way %{direction: "one-way", departure: Date.t(),

    return: Date.t(), errors: []} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "one-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: []} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "one-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:return]} %{direction: "one-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "one-way", departure: nil, return: nil, errors: []} %{direction: "one-way", departure: nil, return: nil, errors: [:return]} %{direction: "one-way", departure: nil, return: nil, errors: [:departure, :return]} # two-way %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:return]} %{direction: "two-way", departure: Date.t(), return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: []} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:return]} %{direction: "two-way", departure: nil, return: Date.t(), errors: [:departure, :return]} %{direction: "two-way", departure: Date.t(), return: nil, errors: []} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure]} %{direction: "two-way", departure: Date.t(), return: nil, errors: [:departure, :return]} %{direction: "two-way", departure: nil, return: nil, errors: []} %{direction: "two-way", departure: nil, return: nil, errors: [:departure]} %{direction: "two-way", departure: nil, return: nil, errors: [:return]}
  148. 26 INVALID STATES REMOVED

  149. WE CANNOT MODEL EVERYTHING

  150. WE CANNOT MODEL EVERYTHING ▸ Validate what you cannot constrain

    with modeling
  151. VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,

    return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end
  152. VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,

    return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end
  153. VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,

    return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end
  154. VALIDATE WHAT YOU CANNOT CONSTRAIN WITH MODELING def book_trip({:two_way, departure,

    return}) do if Date.compare(departure, return) == :gt do {:error, "Departure cannot be after return"} else {:ok, "You have booked a two-way flight departing #{departure} and returning #{return}"} end end
  155. !

  156. WHAT ABOUT ECTO?

  157. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms

  158. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸

    Great casting helpers
  159. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸

    Great casting helpers ▸ Great validation helpers
  160. WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸

    Great casting helpers ▸ Great validation helpers ▸ Use schemaless changesets as an anti-corruption layer
  161. WHAT ABOUT ECTO? Use Ecto as much as you can

  162. WHAT ABOUT ECTO? ! Ecto shines with maps (and structs)

  163. COMBINE SUM TYPES WITH ECTO CHANGESETS

  164. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    date_or_error} | {:two_way, date_or_error, date_or_error}
  165. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    date_or_error} | {:two_way, date_or_error, date_or_error}
  166. COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way,

    Ecto.Changeset.t()} | {:two_way, Ecto.Changeset.t()}
  167. USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do departure_value

    |> parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  168. USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do departure_value

    |> parse_date() |> build_one_way() end def two_way(departure_value, return_value) do departure = parse_date(departure_value) return = parse_date(return_value) build_two_way(departure, return) end
  169. USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do %OneWay{}

    |> OneWay.changeset(%{departure: departure_value}) |> build_one_way() end def two_way(departure_value, return_value) do %TwoWay{} |> TwoWay.changeset(%{departure: departure_value, return: return_value}) |> build_two_way() end
  170. USE ECTO'S CASTING & ERROR GENERATION def one_way(departure_value) do %OneWay{}

    |> OneWay.changeset(%{departure: departure_value}) |> build_one_way() end def two_way(departure_value, return_value) do %TwoWay{} |> TwoWay.changeset(%{departure: departure_value, return: return_value}) |> build_two_way() end
  171. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  172. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  173. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  174. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  175. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  176. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  177. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "one-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <%= select :booker, :flight_type, ["one-way", "two-way"], phx_click: "change-flight-type", value: "two-way", id: "flight-type" %> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  178. RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way,

    changeset} -> %> <button type="button" phx-click="change-flight-type" value="two-way">Two-way</button> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> </.form> <% {:two_way, changeset} -> %> <button type="button" phx-click="change-flight-type" value="one-way">One-way</button> <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= text_input f, :return %> <%= error_tag f, :return %> <%= submit "Book", id: "book-flight" %> </.form> <% end %>
  179. HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)

    do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end
  180. HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)

    do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end
  181. HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)

    do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end
  182. HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)

    do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end
  183. HANDLE CHANGE OF STATE def handle_event("change-flight-type", %{"value" => "one-way"}, socket)

    do date = Date.utc_today() booker = FlightBooker.one_way(date) socket |> assign(:booker, booker) |> noreply() end def handle_event("change-flight-type", %{"value" => "two-way"}, socket) do date = Date.utc_today() booker = FlightBooker.two_way(date, date) socket |> assign(:booker, booker) |> noreply() end
  184. HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"

    => departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end
  185. HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"

    => departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end
  186. HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"

    => departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end
  187. HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"

    => departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end
  188. HANDLE FORMS def handle_event("update", %{"one_way" => params}, socket) do %{"departure"

    => departure} = params booker = FlightBooker.one_way(departure) socket |> assign(:booker, booker) |> noreply() end def handle_event("update", %{"two_way" => params}, socket) do %{"departure" => departure, "return" => return} = params booker = FlightBooker.two_way(departure, return) socket |> assign(:booker, booker) |> noreply() end
  189. ! EXPLORATION NOTES

  190. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states
  191. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively
  192. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values
  193. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values ▸ ✅ Validate what you cannot constrain
  194. ! EXPLORATION NOTES ▸ Model your domain to remove invalid

    states ▸ ! Render complex types Declaratively & Exhaustively ▸ Parse raw ⚠ unsafe values into your domain values ▸ ✅ Validate what you cannot constrain ▸ Combine powers with Ecto $ when dealing with maps
  195. GERMAN VELASCO GERMANVELASCO.COM

  196. THANK YOU GERMANVELASCO.COM