Sure. Here is a very simplified bit of code to demonstrate.
defmodule Foo
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :a, :integer, default: 0
field :b, :integer, default: 0
# calculated field
field :x, :integer, default: 0
end
def changeset(%__MODULE__{} = foo, params \\ %{}) do
foo
|> cast(params, [:a, :b, :x])
end
def calculate(%__MODULE__{} = foo) do
%{ foo | x: foo[:a] + foo[:b] }
end
end
So, when working with LiveView I create a changeset (Foo.changeset(%Foo{}) pass that to to_form and it gets assigned to the socket… standard stuff. When the form on the web page changes, say via def handle_event("change", %{"foo" => changes}, socket) I use the changes to get a changeset. Foo.changeset(%Foo{}, changes). Now I need to run it through calculate to update x and send it back to the webpage. But calculate takes a %Foo{} not a changeset of it.
So what to do? One way to to write another calculate function that can take a changeset of Foo. Something like…
def calculate(%Ecto.Changeset{} = cs_foo) do
a = get_field(cs_foo, :a)
b = get_field(cs_foo, :b)
cs_foo |>
put_change(:x, a + b)
end
The downside of course if lack of DRY – I am implementing the same functionality twice.
The alternative is to apply the changeset, call calculate and make a new changeset.
cs_foo = Foo.changeset(%Foo{}, changes)
case apply_action(cs_foo, :update) do
{:ok, data} ->
new_foo = Foo.calculate(data)
changeset = Foo.changeset(new_foo)
{:noreply, socket |> assign(:form, to_form(changeset))}
{:error, changeset} ->
{:noreply, socket |> assign(:form, to_form(changeset))}
end
This works, but it applies an update I am not necessarily ready to apply (certainly not to the database) and it seems a rather “long way round” just to get back to an updated changeset.
So anyhow, I hope that clarifies things. My thought was maybe I could wrap that changeset in an Access behavior so I can pass it to the same calculate function that the struct itself uses. That’s where the idea of this thread spawned.
(Note I actually was able to implement this Access behavior, at least in part, but as I point out it is an imperfect polymorphism). Here is the code thus far:
defmodule Ecto.Accessor do
defstruct changeset: %{}
@behaviour Access
def fetch(accessor, key) do
changeset = accessor.changeset
case Ecto.Changeset.fetch_field(changeset, key) do
{_from, value} -> {:ok, value}
:error -> :error
end
end
def get_and_update(accessor, key, fun) when is_function(fun, 1) do
changeset = accessor.changeset
current =
case Ecto.Changeset.fetch_field(changeset, key) do
{_from, value} -> value
:error -> nil
end
case fun.(current) do
{get, update} ->
# Hmmm.... should we cast? If so, how?
{get, Ecto.Changeset.put_change(changeset, key, update)}
:pop ->
# doesn't delete the field, just deletes any pending change to it's value
{current, Ecto.Changeset.delete_change(changeset, key)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
def pop(accessor, key, default \\ nil) do
changeset = accessor.changeset
case Ecto.Changeset.fetch_field(changeset, key) do
{_from, value} ->
{value, Ecto.Changeset.delete_change(changeset, key)}
:error ->
{default, changeset}
end
end
end