Conditionally construct map

So I have found a pothole and I am not sure what the solution is.

I have a pretty standard JSON api in phoenix, I am using ecto, and I am mapping those to maps inside the ThingJSON this is all pretty standard.

Now depending on the route, the controller might decide it doesn’t want to preload some data. For example

%Aos.Schema.Shipyard{
  __meta__: #Ecto.Schema.Metadata<:loaded, "shipyard">,
  id: "103b0451-7396-4f11-9cf8-7081c1829f5e",
  port_id: "a748b9d2-40ff-4f14-8c8c-258b343145eb",
  port: %Aos.Schema.Port{
    __meta__: #Ecto.Schema.Metadata<:loaded, "port">,
    id: "a748b9d2-40ff-4f14-8c8c-258b343145eb",
    name: "London",
    shortcode: "lond",
    destinations: #Ecto.Association.NotLoaded<association :destinations is not loaded>,
    ships: #Ecto.Association.NotLoaded<association :ships is not loaded>,
    shipyard: #Ecto.Association.NotLoaded<association :shipyard is not loaded>,
    agents: #Ecto.Association.NotLoaded<association :agents is not loaded>,
    inserted_at: ~U[2024-07-01 02:02:57Z],
    updated_at: ~U[2024-07-01 02:02:57Z]
  },
  ships: [
    %Aos.Schema.ShipyardStock{
      __meta__: #Ecto.Schema.Metadata<:loaded, "shipyard_stock">,
      id: "8117b318-b0ef-4aa7-be31-88437acbe8fc",
      shipyard_id: "103b0451-7396-4f11-9cf8-7081c1829f5e",
      shipyard: #Ecto.Association.NotLoaded<association :shipyard is not loaded>,
      ship_id: "0a238468-1cf0-47b4-b891-d5936d6554a8",
      ship: #Ecto.Association.NotLoaded<association :ship is not loaded>,
      cost: 100,
      inserted_at: ~U[2024-07-01 02:02:57Z],
      updated_at: ~U[2024-07-01 02:02:57Z]
    }
  ],
  inserted_at: ~U[2024-07-01 02:02:57Z],
  updated_at: ~U[2024-07-01 02:02:57Z]
}

On one route it might want to preload the :ships but on another it might not want to.

I have some pretty standard view code that looks like this


  def render("shipyards.json", %{page: page}) do
    %{
      data: for(yard <- page.entries, do: shipyard(yard)),
      meta: %{
        page_number: page.page_number,
        page_size: page.page_size,
        total_entries: page.total_entries,
        total_pages: page.total_pages
      }
    }
  end

  def shipyard(shipyard) do
    %{
      id: shipyard.id,
      port: PortJSON.port(shipyard.port)
    }
  end

Now I want to modify the shipyard function to also display the ships but only if the assoc is actually preloaded.

Okay so I think

  def shipyard(shipyard) do
    %{
      id: shipyard.id,
      port: PortJSON.port(shipyard.port),
      ships:
        if Ecto.assoc_loaded?(shipyard.ships) do
          for(entry <- shipyard.ships, do: stock(entry))
        else
          # ???
        end
    }
  end

Problem is, if ships is not loaded it will export json of ships: null which is misleading IMO. I’d rather it just omitted the key.

I could do something like this

  def render_if(map, key, assoc, render) do
    if Ecto.assoc_loaded?(assoc) do
      Map.put(map, key, render.(assoc))
    else
      map
    end
  end

  def shipyard(shipyard) do
    %{
      id: shipyard.id,
      port: PortJSON.port(shipyard.port)
    }
    |> render_if(:ships, shipyard.ships, &stock/1)
  end

But it feels kinda wrong.

Also why is Ecto.assoc_loaded? not available as a guard.

Anyone got any ideas?