Skip to content

Commit

Permalink
feat(TripPlanner.Form): use Ecto.Schema to create a form (#2159)
Browse files Browse the repository at this point in the history
  • Loading branch information
thecristen authored Aug 27, 2024
1 parent 2ce5e4c commit 775f1fe
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 0 deletions.
111 changes: 111 additions & 0 deletions lib/trip_planner/input_form.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
defmodule TripPlanner.InputForm do
@moduledoc """
Describes the inputs users can fill to request trip plans.
At minimum, this requires two locations.
"""

use TypedEctoSchema
import Ecto.Changeset

@error_messages %{
from: "Please specify an origin location.",
to: "Please add a destination.",
from_to_same: "Please select a destination at a different location from the origin."
}

@primary_key false
typed_embedded_schema do
embeds_one(:from, __MODULE__.Location)
embeds_one(:to, __MODULE__.Location)
end

def changeset(params \\ %{}) do
changeset(%__MODULE__{}, params)
end

def changeset(form, params) do
form
|> cast(params, [])
|> cast_embed(:from, required: true)
|> cast_embed(:to, required: true)
end

def validate_params(params) do
changes =
params
|> changeset()
|> update_change(:from, &update_location_change/1)
|> update_change(:to, &update_location_change/1)
|> validate_required(:from, message: error_message(:from))
|> validate_required(:to, message: error_message(:to))
|> validate_same_locations()

if changes.errors == [] do
changes
else
%Ecto.Changeset{changes | action: :update}
end
end

# make the parent field blank if the location isn't valid
defp update_location_change(%Ecto.Changeset{valid?: false, errors: [_ | _]}), do: nil
defp update_location_change(changeset), do: changeset

defp validate_same_locations(changeset) do
with from_change when not is_nil(from_change) <- get_change(changeset, :from),
to_change when to_change === from_change <- get_change(changeset, :to) do
add_error(
changeset,
:to,
error_message(:from_to_same)
)
else
_ ->
changeset
end
end

def error_message(key), do: @error_messages[key]

defmodule Location do
@moduledoc """
Represents a location for requesting a trip plan. At minimum, coordinates
are required. A stop_id is expected to be associated with an MBTA GTFS stop.
If a name is not provided, one can be created based on the coordinates.
"""

use TypedEctoSchema

@primary_key false
typed_embedded_schema do
field(:latitude, :float, null: false)
field(:longitude, :float, null: false)
field(:name, :string)
field(:stop_id, :string) :: Stops.Stop.id_t()
end

def changeset(form \\ %__MODULE__{}, params \\ %{}) do
form
|> cast(params, [:latitude, :longitude, :name, :stop_id])
|> validate_required([:latitude, :longitude])
|> add_default_name()
end

defp add_default_name(changeset) do
if is_nil(changeset.data.name) and
(changed?(changeset, :latitude) or changed?(changeset, :longitude)) do
{_, latitude} = fetch_field(changeset, :latitude)
{_, longitude} = fetch_field(changeset, :longitude)

put_change(
changeset,
:name,
"#{latitude}, #{longitude}"
)
else
changeset
end
end
end
end
3 changes: 3 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ defmodule DotCom.Mixfile do
{:decorator, "1.4.0"},
{:dialyxir, "1.4.3", [only: [:test, :dev], runtime: false]},
{:diskusage_logger, "0.2.0"},
{:ecto, "3.12.1"},
{:eflame, "1.0.1", only: :dev},
{:ehmon, [github: "mbta/ehmon", only: :prod]},
{:ex_aws, "2.5.4"},
Expand Down Expand Up @@ -106,6 +107,7 @@ defmodule DotCom.Mixfile do
{:parallel_stream, "1.1.0"},
# latest version 1.7.14
{:phoenix, "~> 1.7"},
{:phoenix_ecto, "4.6.2"},
# latest version 4.1.1; cannot upgrade because we use Phoenix.HTML
{:phoenix_html, "3.3.3"},
{:phoenix_live_dashboard, "0.8.4"},
Expand Down Expand Up @@ -142,6 +144,7 @@ defmodule DotCom.Mixfile do
{:telemetry_poller, "1.1.0"},
{:telemetry_test, "0.1.2", only: [:test]},
{:timex, "3.7.11"},
{:typed_ecto_schema, "0.4.1"},
{:unrooted_polytree, "0.1.1"},
{:uuid, "1.1.8"},
{:wallaby, "0.30.9", [runtime: false, only: [:test, :dev]]},
Expand Down
4 changes: 4 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"crc": {:hex, :crc, "0.10.5", "ee12a7c056ac498ef2ea985ecdc9fa53c1bfb4e53a484d9f17ff94803707dfd8", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3e673b6495a9525c5c641585af1accba59a1eb33de697bedf341e247012c2c7f"},
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"csv": {:hex, :csv, "3.2.1", "6d401f1ed33acb2627682a9ab6021e96d33ca6c1c6bccc243d8f7e2197d032f5", [:mix], [], "hexpm", "8f55a0524923ae49e97ff2642122a2ce7c61e159e7fe1184670b2ce847aee6c8"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"diskusage_logger": {:hex, :diskusage_logger, "0.2.0", "04fc48b538fe4de43153542a71ea94f623d54707d85844123baacfceedf625c3", [:mix], [], "hexpm", "e3f2aed1b0fc4590931c089a6453a4c4eb4c945912aa97bcabcc0cff7851f34d"},
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
"ecto": {:hex, :ecto, "3.12.1", "626765f7066589de6fa09e0876a253ff60c3d00870dd3a1cd696e2ba67bfceea", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df0045ab9d87be947228e05a8d153f3e06e0d05ab10c3b3cc557d2f7243d1940"},
"eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"},
"ehmon": {:git, "https://github.com/mbta/ehmon.git", "1fb603262bd02d74a16183bd8f344dcace9d7561", []},
"elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
Expand Down Expand Up @@ -65,6 +67,7 @@
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
Expand Down Expand Up @@ -101,6 +104,7 @@
"telemetry_test": {:hex, :telemetry_test, "0.1.2", "122d927567c563cf57773105fa8104ae4299718ec2cbdddcf6776562c7488072", [:mix], [{:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7bd41a49ecfd33ecd82d2c7edae19a5736f0d2150206d0ee290dcf3885d0e14d"},
"tesla": {:hex, :tesla, "1.11.2", "24707ac48b52f72f88fc05d242b1c59a85d1ee6f16f19c312d7d3419665c9cd5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c549cd03aec6a7196a641689dd378b799e635eb393f689b4bd756f750c7a4014"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"},
"typed_struct": {:hex, :typed_struct, "0.2.1", "e1993414c371f09ff25231393b6430bd89d780e2a499ae3b2d2b00852f593d97", [:mix], [], "hexpm", "8f5218c35ec38262f627b2c522542f1eae41f625f92649c0af701a6fab2e11b3"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
Expand Down
87 changes: 87 additions & 0 deletions test/trip_planner/input_form_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
defmodule TripPlanner.InputFormTest do
use ExUnit.Case, async: true

alias TripPlanner.InputForm

@from_params %{
"latitude" => "#{Faker.Address.latitude()}",
"longitude" => "#{Faker.Address.longitude()}"
}
@to_params %{
"latitude" => "#{Faker.Address.latitude()}",
"longitude" => "#{Faker.Address.longitude()}"
}
@params %{
"from" => @from_params,
"to" => @to_params
}

test "from & to fields are required" do
changeset = InputForm.changeset(%{})
assert {_, [validation: :required]} = changeset.errors[:from]
assert {_, [validation: :required]} = changeset.errors[:to]
end

describe "validate_params/1" do
test "validates to & from" do
changeset = InputForm.validate_params(@params)
assert changeset.valid?
end

test "adds from & to errors" do
changeset =
InputForm.validate_params(%{
"from" => %{
"latitude" => "",
"longitude" => "",
"name" => "",
"stop_id" => ""
},
"to" => %{
"latitude" => "",
"longitude" => "",
"name" => "",
"stop_id" => ""
}
})

refute changeset.valid?
assert changeset.errors[:to]
assert changeset.errors[:from]
end

test "adds error if from & to are the same" do
changeset =
InputForm.validate_params(%{
"from" => @from_params,
"to" => @from_params
})

refute changeset.valid?

expected_error = InputForm.error_message(:from_to_same)
assert {^expected_error, _} = changeset.errors[:to]
end
end

describe "Location" do
test "longitude & latitude fields are required" do
changeset = InputForm.Location.changeset()
assert {_, [validation: :required]} = changeset.errors[:latitude]
assert {_, [validation: :required]} = changeset.errors[:longitude]
end

test "adds a name if none provided" do
lat = Faker.Address.latitude()
lon = Faker.Address.longitude()

changeset =
InputForm.Location.changeset(%InputForm.Location{}, %{
"latitude" => "#{lat}",
"longitude" => "#{lon}"
})

assert changeset.changes.name == "#{lat}, #{lon}"
end
end
end

0 comments on commit 775f1fe

Please sign in to comment.