From 046c4d47a913130bb93176cc8fe592f422031af3 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 10 Jun 2020 19:05:37 -0400 Subject: [PATCH] Use keypaths for stage option editing --- lib/frenzy/keypath.ex | 73 +++++++++++++++++++ .../live/configure_stage/scrape_stage_live.ex | 11 ++- .../scrape_stage_live.html.leex | 4 +- lib/frenzy_web/live/edit_pipeline_live.ex | 34 ++++++--- test/frenzy/keypath_test.exs | 4 + 5 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 lib/frenzy/keypath.ex create mode 100644 test/frenzy/keypath_test.exs diff --git a/lib/frenzy/keypath.ex b/lib/frenzy/keypath.ex new file mode 100644 index 0000000..07236d5 --- /dev/null +++ b/lib/frenzy/keypath.ex @@ -0,0 +1,73 @@ +defmodule Frenzy.Keypath do + @moduledoc """ + Utilities for accessing or updating values using keypaths. + A keypath is a list of map keys or list indices representing the path through nested maps/lists. + """ + + @type container() :: map() | list() + @type t() :: [Map.key() | non_neg_integer()] + + @doc """ + Gets the value in the given container at the given keypath. Raises on index out of bounds/missing map key. + + ## Examples + + iex> Frenzy.Keypath.get(%{foo: %{bar: "baz"}}, [:foo, :bar]) + "baz" + + iex> Frenzy.Keypath.get([%{"foo" => "bar"}, %{"foo" => "baz"}], [1, "foo"]) + "baz" + """ + @spec get(container(), t()) :: any() + + def get(value, []), do: value + + def get(map, [key | rest]) when is_map(map) do + get(Map.fetch!(map, key), rest) + end + + def get(list, [index | rest]) when is_list(list) and is_integer(index) and index >= 0 do + if index >= length(list) do + raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" + else + get(Enum.at(list, index), rest) + end + end + + @doc """ + Sets the vlaue in the given container at the given keypath. Raises on index out of bounds/missing map key. + + ## Examples + + iex> Frenzy.Keypath.set(%{foo: %{bar: "baz"}}, [:foo, :bar], "blah") + %{foo: %{bar: "blah"}} + + iex> Frenzy.Keypath.set([%{"foo" => "bar"}, %{"list" => ["a", "b"]}], [1, "list", 0], "c") + [%{"foo" => "bar"}, %{"list" => ["c", "b"]}] + """ + @spec set(container(), t(), any()) :: map() + + def set(map, [key], value) when is_map(map) do + Map.put(map, key, value) + end + + def set(list, [index], value) when is_list(list) and is_integer(index) and index >= 0 do + if index >= length(list) do + raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" + else + List.replace_at(list, index, value) + end + end + + def set(map, [key | rest], value) when is_map(map) do + Map.put(map, key, set(Map.fetch!(map, key), rest, value)) + end + + def set(list, [index | rest], value) when is_list(list) and is_integer(index) and index >= 0 do + if index >= length(list) do + raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" + else + List.replace_at(list, index, set(Enum.at(list, index), rest, value)) + end + end +end diff --git a/lib/frenzy_web/live/configure_stage/scrape_stage_live.ex b/lib/frenzy_web/live/configure_stage/scrape_stage_live.ex index a4d4ba6..0b68275 100644 --- a/lib/frenzy_web/live/configure_stage/scrape_stage_live.ex +++ b/lib/frenzy_web/live/configure_stage/scrape_stage_live.ex @@ -1,6 +1,5 @@ defmodule FrenzyWeb.ConfigureStage.ScrapeStageLive do use FrenzyWeb, :live_component - alias Frenzy.JSONSchema @extractors [ {"Builtin", "builtin"}, @@ -32,6 +31,12 @@ defmodule FrenzyWeb.ConfigureStage.ScrapeStageLive do {:ok, assign(socket, extractors: @extractors)} end + @impl true + def update(assigns, socket) do + assigns = Map.put(assigns, :opts, Frenzy.Keypath.get(assigns.stage, assigns.keypath)) + {:ok, assign(socket, assigns)} + end + @impl true def handle_event( "update_stage", @@ -45,7 +50,9 @@ defmodule FrenzyWeb.ConfigureStage.ScrapeStageLive do |> Map.put("convert_to_data_uris", convert_to_data_uris) |> Map.put("extractor", extractor) - send(self(), {:update_stage_opts, socket.assigns.index, new_opts}) + new_stage = Frenzy.Keypath.set(socket.assigns.stage, socket.assigns.keypath, new_opts) + + send(self(), {:update_stage, socket.assigns.index, new_stage}) {:noreply, socket} end end diff --git a/lib/frenzy_web/live/configure_stage/scrape_stage_live.html.leex b/lib/frenzy_web/live/configure_stage/scrape_stage_live.html.leex index 72c4eff..6d352db 100644 --- a/lib/frenzy_web/live/configure_stage/scrape_stage_live.html.leex +++ b/lib/frenzy_web/live/configure_stage/scrape_stage_live.html.leex @@ -1,5 +1,7 @@
-
<%= Jason.encode!(@opts, pretty: true) %>
+ <%= if Mix.env == :dev do %> +
<%= Jason.encode!(@opts, pretty: true) %>
+ <% end %> <%= f = form_for @opts, "#", [as: :opts, phx_change: :update_stage, phx_target: @myself] %>
<%= checkbox f, :convert_to_data_uris, id: "#{@id}-convert_to_data_uris", class: "form-check-input" %> diff --git a/lib/frenzy_web/live/edit_pipeline_live.ex b/lib/frenzy_web/live/edit_pipeline_live.ex index 66a90fb..0091f6e 100644 --- a/lib/frenzy_web/live/edit_pipeline_live.ex +++ b/lib/frenzy_web/live/edit_pipeline_live.ex @@ -3,6 +3,12 @@ defmodule FrenzyWeb.EditPipelineLive do use Phoenix.HTML alias Frenzy.{Repo, Pipeline} + @stages [ + {"Filter Stage", "Frenzy.Pipeline.FilterStage"}, + {"Scrape Stage", "Frenzy.Pipeline.ScrapeStage"}, + {"Conditional Stage", "Frenzy.Pipeline.ConditionalStage"} + ] + @impl true def mount(%{"id" => pipeline_id}, _session, socket) do pipeline = Repo.get(Pipeline, pipeline_id) @@ -10,11 +16,7 @@ defmodule FrenzyWeb.EditPipelineLive do {:ok, assign(socket, pipeline: pipeline, - stages: [ - {"Filter Stage", "Frenzy.Pipeline.FilterStage"}, - {"Scrape Stage", "Frenzy.Pipeline.ScrapeStage"}, - {"Conditional Stage", "Frenzy.Pipeline.ConditionalStage"} - ] + stages: @stages )} end @@ -72,23 +74,26 @@ defmodule FrenzyWeb.EditPipelineLive do end @impl true - def handle_info({:update_stage_opts, index, new_opts}, socket) do + def handle_info({:update_stage, index, new_stage}, socket) do pipeline = socket.assigns.pipeline - stages = pipeline.stages - stage = Enum.at(stages, index) - new_stage = Map.put(stage, "options", new_opts) - new_stages = List.replace_at(stages, index, new_stage) + # stages = pipeline.stages + # stage = Enum.at(stages, index) + # new_stage = Map.put(stage, "options", new_opts) + new_stages = List.replace_at(pipeline.stages, index, new_stage) changeset = Pipeline.changeset(pipeline, %{stages: new_stages}) {:ok, pipeline} = Repo.update(changeset) {:noreply, assign(socket, pipeline: pipeline)} end - def component_for(socket, %{"module_name" => module, "options" => opts}, index) do + def component_for(socket, %{"module_name" => module} = stage, index) do component = case module do "Frenzy.Pipeline.ScrapeStage" -> FrenzyWeb.ConfigureStage.ScrapeStageLive + "Frenzy.Pipeline.ConditionalStage" -> + FrenzyWeb.ConfigureStage.ConditionalStageLive + _ -> nil end @@ -98,7 +103,12 @@ defmodule FrenzyWeb.EditPipelineLive do nil component -> - live_component(socket, component, index: index, id: "stage-#{index}", opts: opts) + live_component(socket, component, + index: index, + id: "stage-#{index}", + stage: stage, + keypath: ["options"] + ) end end end diff --git a/test/frenzy/keypath_test.exs b/test/frenzy/keypath_test.exs new file mode 100644 index 0000000..5c2e9a4 --- /dev/null +++ b/test/frenzy/keypath_test.exs @@ -0,0 +1,4 @@ +defmodule Frenzy.KeypathTest do + use ExUnit.Case + doctest Frenzy.Keypath +end