Slide 1

Slide 1 text

MAKING INVALID STATES INVALID in LiveView

Slide 2

Slide 2 text

MAKING INVALID STATES INVALID in LiveView AN EXPLORATION

Slide 3

Slide 3 text

WHAT DOES THAT MEAN?

Slide 4

Slide 4 text

How we model our domain constrains what states are allowed

Slide 5

Slide 5 text

Model our domain to remove states we don't want

Slide 6

Slide 6 text

NOT NEW

Slide 7

Slide 7 text

NOT NEW ▸ OCaml

Slide 8

Slide 8 text

NOT NEW ▸ OCaml ▸ Elm

Slide 9

Slide 9 text

NOT NEW ▸ OCaml ▸ Elm ▸ F#

Slide 10

Slide 10 text

OTHER NAMES

Slide 11

Slide 11 text

OTHER NAMES ▸ Making invalid states unrepresentable

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

OTHER NAMES ▸ Making invalid states unrepresentable ▸ Making illegal states unrepresentable ▸ Making impossible states impossible

Slide 14

Slide 14 text

BRINGING IT TO LiveView

Slide 15

Slide 15 text

BRINGING IT TO LiveView ▸ Persistent state

Slide 16

Slide 16 text

BRINGING IT TO LiveView ▸ Persistent state ▸ That our users see

Slide 17

Slide 17 text

A FLIGHT BOOKER EXAMPLE !

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

FIRST IMPLEMENTATION

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

! THE BUGS

Slide 33

Slide 33 text

! THE BUGS

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

! THE BUGS ▸ ! One-way flight that got charged for a return flight ▸ " Two-way flight without a return flight

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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}

Slide 42

Slide 42 text

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}

Slide 43

Slide 43 text

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}

Slide 44

Slide 44 text

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}

Slide 45

Slide 45 text

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}

Slide 46

Slide 46 text

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()}

Slide 47

Slide 47 text

A MORE constrained FLIGHT BOOKER DOMAIN MODEL

Slide 48

Slide 48 text

A MORE constrained FLIGHT BOOKER DOMAIN MODEL

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

A MORE constrained FLIGHT BOOKER DOMAIN MODEL ▸ One-way flight with departure date ▸ OR ▸ Two-way flight with departure and return dates

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

IMPLICATIONS OF OUR NEW DOMAIN MODEL

Slide 62

Slide 62 text

IMPLICATIONS OF OUR NEW DOMAIN MODEL ▸ Render complex types

Slide 63

Slide 63 text

IMPLICATIONS OF OUR NEW DOMAIN MODEL ▸ Render complex types ▸ Handle events with user input

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

1. RENDER COMPLEX TYPE <%= 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" %>

Slide 67

Slide 67 text

1. RENDER COMPLEX TYPE <%= 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" %>

Slide 68

Slide 68 text

1. RENDER COMPLEX TYPE <%= 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" %>

Slide 69

Slide 69 text

1. RENDER COMPLEX TYPE <%= 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" %>

Slide 70

Slide 70 text

1. RENDER COMPLEX TYPE <%= 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" %>

Slide 71

Slide 71 text

FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <%= 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" %>

Slide 72

Slide 72 text

FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <%= 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" %>

Slide 73

Slide 73 text

FORCED TO HANDLE RETURN ON "ONE-WAY" FLIGHT <%= 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" %>

Slide 74

Slide 74 text

INVALID STATE REMOVED %{ direction: "one-way", departure: Date.t(), return: Date.t() }

Slide 75

Slide 75 text

2. HANDLE EVENTS

Slide 76

Slide 76 text

TRANSFORM USER INPUT THROUGH PARSING

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

INVALID STATE REMOVED %{ direction: "two-way", departure: Date.t(), return: nil }

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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()}

Slide 93

Slide 93 text

Without any error handling, we have removed 6 invalid states!

Slide 94

Slide 94 text

WHAT ABOUT ERROR HANDLING?

Slide 95

Slide 95 text

WHAT ABOUT ERROR HANDLING? ▸ Validate and present errors

Slide 96

Slide 96 text

WHAT ABOUT ERROR HANDLING? ▸ Validate and present errors ▸ Potential for same problems

Slide 97

Slide 97 text

POSSIBLE ERRORS @type error_fields :: :departure | :return @type errors :: [error_fields]

Slide 98

Slide 98 text

ADDING ERRORS ASSIGN socket |> assign(:direction, "one-way") |> assign(:departure, Date.utc_today()) |> assign(:return, nil) + |> assign(:errors, [])

Slide 99

Slide 99 text

(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}

Slide 100

Slide 100 text

(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}

Slide 101

Slide 101 text

# 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}

Slide 102

Slide 102 text

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]}

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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]}

Slide 106

Slide 106 text

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]}

Slide 107

Slide 107 text

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]}

Slide 108

Slide 108 text

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]}

Slide 109

Slide 109 text

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]}

Slide 110

Slide 110 text

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}

Slide 111

Slide 111 text

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}

Slide 112

Slide 112 text

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}

Slide 113

Slide 113 text

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}

Slide 114

Slide 114 text

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}

Slide 115

Slide 115 text

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()}}

Slide 116

Slide 116 text

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()}}

Slide 117

Slide 117 text

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()}}

Slide 118

Slide 118 text

RENDERING COMPLEX TYPES <%= 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" %>

Slide 119

Slide 119 text

RENDERING COMPLEX TYPES <%= 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" %>

Slide 120

Slide 120 text

RENDERING COMPLEX TYPES <%= 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" %>

Slide 121

Slide 121 text

RENDERING COMPLEX TYPES <%= 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" %>

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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}

Slide 138

Slide 138 text

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}

Slide 139

Slide 139 text

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}

Slide 140

Slide 140 text

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}

Slide 141

Slide 141 text

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}

Slide 142

Slide 142 text

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}

Slide 143

Slide 143 text

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}

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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]}

Slide 148

Slide 148 text

26 INVALID STATES REMOVED

Slide 149

Slide 149 text

WE CANNOT MODEL EVERYTHING

Slide 150

Slide 150 text

WE CANNOT MODEL EVERYTHING ▸ Validate what you cannot constrain with modeling

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

!

Slide 156

Slide 156 text

WHAT ABOUT ECTO?

Slide 157

Slide 157 text

WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

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

Slide 160

Slide 160 text

WHAT ABOUT ECTO? ▸ Well integrated with Phoenix forms ▸ Great casting helpers ▸ Great validation helpers ▸ Use schemaless changesets as an anti-corruption layer

Slide 161

Slide 161 text

WHAT ABOUT ECTO? Use Ecto as much as you can

Slide 162

Slide 162 text

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

Slide 163

Slide 163 text

COMBINE SUM TYPES WITH ECTO CHANGESETS

Slide 164

Slide 164 text

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

Slide 165

Slide 165 text

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

Slide 166

Slide 166 text

COMBINE SUM TYPES WITH ECTO CHANGESETS @type flight_booker :: {:one_way, Ecto.Changeset.t()} | {:two_way, Ecto.Changeset.t()}

Slide 167

Slide 167 text

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

Slide 168

Slide 168 text

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

Slide 169

Slide 169 text

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

Slide 170

Slide 170 text

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

Slide 171

Slide 171 text

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" %> <% {: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" %> <% end %>

Slide 172

Slide 172 text

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" %> <% {: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" %> <% end %>

Slide 173

Slide 173 text

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" %> <% {: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" %> <% end %>

Slide 174

Slide 174 text

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" %> <% {: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" %> <% end %>

Slide 175

Slide 175 text

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" %> <% {: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" %> <% end %>

Slide 176

Slide 176 text

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" %> <% {: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" %> <% end %>

Slide 177

Slide 177 text

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" %> <% {: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" %> <% end %>

Slide 178

Slide 178 text

RENDERING COMPLEX TYPES <%= case @booker do %> <% {:one_way, changeset} -> %> Two-way <.form let={f} for={changeset} phx-submit="book" phx-change="update"> <%= text_input f, :departure %> <%= error_tag f, :departure %> <%= submit "Book", id: "book-flight" %> <% {:two_way, changeset} -> %> One-way <.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" %> <% end %>

Slide 179

Slide 179 text

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

Slide 180

Slide 180 text

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

Slide 181

Slide 181 text

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

Slide 182

Slide 182 text

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

Slide 183

Slide 183 text

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

Slide 184

Slide 184 text

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

Slide 185

Slide 185 text

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

Slide 186

Slide 186 text

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

Slide 187

Slide 187 text

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

Slide 188

Slide 188 text

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

Slide 189

Slide 189 text

! EXPLORATION NOTES

Slide 190

Slide 190 text

! EXPLORATION NOTES ▸ Model your domain to remove invalid states

Slide 191

Slide 191 text

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

Slide 192

Slide 192 text

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

Slide 193

Slide 193 text

! 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

Slide 194

Slide 194 text

! 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

Slide 195

Slide 195 text

GERMAN VELASCO GERMANVELASCO.COM

Slide 196

Slide 196 text

THANK YOU GERMANVELASCO.COM