Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(TripPlanner.Form): use Ecto.Schema to create a form #2159

Merged
merged 4 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions lib/trip_planner/input_form.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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

@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(input_form, params) do
input_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: "Please specify an origin location.")
|> validate_required(:to, message: "Please add a destination.")
|> 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,
"Please select a destination at a different location from the origin."
)
else
_ ->
changeset
end
end

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(struct \\ %__MODULE__{}, params \\ %{}) do
struct
|> cast(params, [:latitude, :longitude, :name, :stop_id])
|> add_default_name()
|> validate_required([:latitude, :longitude])
end

defp add_default_name(changeset) do
struct = apply_changes(changeset)

if is_nil(struct.name) and not is_nil(struct.latitude) and not is_nil(struct.longitude) do
put_change(changeset, :name, "#{struct.latitude}, #{struct.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 @@ -143,6 +145,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 @@ -64,6 +66,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 @@ -100,6 +103,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
92 changes: 92 additions & 0 deletions test/trip_planner/input_form_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule TripPlanner.InputFormTest do
use ExUnit.Case, async: true

alias TripPlanner.InputForm

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(%{
"from" => %{
"latitude" => "42",
"longitude" => "23"
},
"to" => %{
"latitude" => "123",
"longitude" => "23423"
}
})

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
location = %{
"latitude" => "#{Faker.Address.latitude()}",
"longitude" => "#{Faker.Address.longitude()}",
"name" => "This place",
"stop_id" => ""
}

changeset =
InputForm.validate_params(%{
"from" => location,
"to" => location
})

refute changeset.valid?

assert {"Please select a destination at a different location from the origin.", _} =
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
Loading