I’m working on a program that needs to display the status of several resources. The dashboard needs to consistently display these resources in the exact order the resources are declared in a config file.
Right now, I’m representing each “resource” as a Struct, and the overall state of the dashboard as a List of these structs, where that list is ordered according to how the resources should display.
defmodule FooApp.Application.Model do
use GenServer
require Logger
defmodule State do
@enforce_keys [:name, :resources]
defstruct [:name, :resources, :_pubsub_topic]
end
defmodule Resource do
@enforce_keys [:name]
defstruct [:name, status: "undefined"]
@allowable_statuses ["green", "green-with-exception", "red"]
def status_ok(status) do
status in @allowable_statuses
end
end
defp list_update_such(list, fun_pred, fun) do
Enum.map(list, &if fun_pred.(&1) do fun.(&1) else &1 end)
end
@impl true
def init({name, resource_names}) do
{:ok, %State{
name: name,
resources: resource_names |> Enum.map(&%Resource{name: &1}),
_pubsub_topic: name
}}
end
@impl true
def handle_continue(:broadcast_statechange, state) do
Phoenix.PubSub.broadcast!(FooApp.PubSub, state._pubsub_topic, {:statechange, state});
{:noreply, state}
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call({:act, actor, action}, from, state) do
case action do
{:set_resource_status, resource_name, new_status} -> (
with \
true <- actor === "director" || {:error, "Unauthorized"},
resources = state.resources,
true <- Resource.status_ok(new_status) || {:error, "Invalid status"}
do
{:reply, :ok, %{state |
resources: list_update_such(resources, &(&1.name === resource_name), &%{&1 | status: new_status})
}, {:continue, :broadcast_statechange}}
else
{:error, error} -> {:reply, {:error, error}, state}
end
)
_ -> (
Logger.warning("Client #{inspect from} attempted invalid action");
{:reply, {:error, "Action incongruent with current state"}, state}
)
end
end
end
Now, I have heard it said that you should “make it work, then make it pretty, then if needed make it fast”. The above works, but that list_update_such
seems like such a horrible kludge. I did it because it was the prettiest option I could pull out of this list of options I am aware of:
- A List
[resource, ...]
is awkward because there isn’t great tooling to update - A List
[{resource_name, resource}, ...]
is strictly more awkward because you have the key in 2 places yet it still doesn’t work with tools likeput_in
when the “keys” are strings; - A Map
%{resource_name => resource, ...}
is more awkward because the key’s duplicated, and also just straight up unallowable because it doesn’t guarantee any ordering; - Expanding the storage to use a separate index, like
%S{resource_ordering: [resource_name, ...], resources: %{resource_name => resource, ...}}
seems complete over-engineering since we’re not likely to have more than 25 or so resources, and still has that extra elegance fine of the keys being stored in multiple places.
Is there any data structure I’m just completely missing, or am I about on the right track here?