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