correct
Hmm … I’m not sure if assuming that only by it is good …
First of all it looks like a typical Phoenix app which is using Ecto.Schema (I guess that there are just virtual fields). The fields are generated only in the controller for show action. Of course there may be something in Clients.get_project!/1, but we can’t be sure about that. If that’s a typical phoenix context function then it’s just a simple query by id.
If we are using struct then put_new/1 would have no affect as project would have default value (or nil) already. We know that those fields are generated based on other fields (imagine full_name = "#{first_name} #{last_name}". In most cases such fields could be update (or more precisely re-generated) several times and therefore more interesting here is if related fields are changing.
Finally Map.put_new/3 is not good for such usage. Look that total_budget/1 and budget_used/1 functions would be called regardless if there would be already value or not - in many cases it causes many problems. When I see such mistakes I just can’t assume such things and I’m simply providing a solution for most typical cases.
The best way is to provide sample input and output right after question, so our examples could be tested properly. Right now we guess that x solution is good or not. That’s said op asked more about code organization than a typical solution. I think that my examples are helpful also in this part.
Regarding my solution it’s really easy to update it by just wrapping do … end block inside an if with simple condition using Map.has_key?/2 …
defmodule Project do
defstruct [:budget, :budget_items, :budget_remaining, :budget_used, :work_items]
def calculate(project) do
fields = [:budget, :budget_used, :budget_remaining]
Enum.reduce(fields, project, fn key, acc ->
if Map.has_key?(project, key) do
project
else
value = Project.calculate(project, key)
Map.put(project, key, value
end
end)
# short notation:
# updated_project = Enum.reduce(fields, project, &Map.put(&2, &1, Project.calculate(&2, &1))
end
def calculate(%{budget_items: items}, :budget), do: total_budget(items)
def calculate(%{work_items: items}, :budget_used), do: budget_used(items)
def calculate(%{budget: budget, budget_used: used}, :budget_remaining), do: budget - used
end
project = id |> Clients.get_project!() |> Project.calculate()
Look that my solution is really easy to change.