Current section
12 Versions
Jump to
Current section
12 Versions
Compare versions
5
files changed
+162
additions
-247
deletions
| @@ -84,7 +84,7 @@ defmodule UserJSON do | |
| 84 84 | links fn user -> |
| 85 85 | %{ |
| 86 86 | FooWeb.TeamJSON => user.team_id, # You can also pass list of ids or the Ecto record(s) |
| 87 | - FooWeb. ProfileJSON => user.profile_id |
| 87 | + FooWeb.ProfileJSON => user.profile_id |
| 88 88 | } |
| 89 89 | end |
| 90 90 | |
| @@ -177,13 +177,6 @@ This example also shows reading & parsing the `include` parameter, which can be | |
| 177 177 | * Empty list (`GET /api/users?included=`): No link types are included. |
| 178 178 | * Custom types: (`GET /api/users/123?include=team,profile`): Include comma-separated link types only. |
| 179 179 | |
| 180 | - ## How does it work? |
| 181 | - |
| 182 | - * Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })` |
| 183 | - * Controller calls these view functions |
| 184 | - * Carve pulls the list of links for given data (list or single record) |
| 185 | - * Carve calls the `get_by_id` (`get` macro expanded) and `prepare_for_view` (`view` macro expanded) functions for each link |
| 186 | - * The final expanded list of links get flattened & cleaned, returned to user with the main result: `{ result: {} || [], links: [] }` |
| 187 180 | |
| 188 181 | ## Links |
| 189 182 | |
| @@ -204,8 +197,6 @@ end | |
| 204 197 | |
| 205 198 | Now, a request to `/api/users/123` automatically includes linked team and company in a single response. Client gets all data needed without extra requests. |
| 206 199 | |
| 207 | - |
| 208 | - |
| 209 200 | Unlike GraphQL which requires defining a schema and writing resolvers for each field, Carve allows you to define links between resources directly in the view. When a user fetches a resource, all necessary context is automatically included in the response: |
| 210 201 | |
| 211 202 | ```elixir |
| @@ -277,19 +268,20 @@ Example response with `?include=team`: | |
| 277 268 | } |
| 278 269 | ``` |
| 279 270 | |
| 280 | - ## Lazy Links |
| 271 | + ### Lazy Links |
| 281 272 | |
| 282 273 | When using `links`, even if a link type is filtered out via `?include=`, database queries are still executed for all linked resources. |
| 283 274 | |
| 284 | - For expensive queries, you can use `lazy_links` to execute them only when explicitly requested: |
| 275 | + For expensive queries, you can simply declare lazy links; |
| 285 276 | |
| 286 277 | ```elixir |
| 287 | - defmodule MyJSON do |
| 288 | - use Carve.View, :resource |
| 278 | + defmodule UserJSON do |
| 279 | + use Carve.View, :user |
| 289 280 | |
| 290 | - lazy_links fn resource -> |
| 281 | + links fn user -> |
| 291 282 | %{ |
| 292 | - comments: fn -> {CommentJSON, Comments.by_user_id(user.id)} end |
| 283 | + TeamJSON => user.team_id, # Included by default |
| 284 | + CommentJSON => fn -> Comments.by_user_id(user.id) end # Called & included only if specified explicitly |
| 293 285 | } |
| 294 286 | end |
| 295 287 | end |
| @@ -325,6 +317,15 @@ links fn user -> | |
| 325 317 | end |
| 326 318 | ``` |
| 327 319 | |
| 320 | + ## How does it work? |
| 321 | + |
| 322 | + * Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })` |
| 323 | + * Controller calls these view functions |
| 324 | + * Carve pulls the list of links for given data (list or single record) |
| 325 | + * Carve calls the `get_by_id` (`get` macro expanded) and `prepare_for_view` (`view` macro expanded) functions for each link |
| 326 | + * The final expanded list of links get flattened & cleaned, returned to user with the main result: `{ result: {} || [], links: [] }` |
| 327 | + |
| 328 | + |
| 328 329 | ## API |
| 329 330 | |
| 330 331 | More detailed API docs are available at [https://hexdocs.pm/carve/Carve.html](https://hexdocs.pm/carve/Carve.html) |
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/azer/carve">>}]}. |
| 2 2 | {<<"name">>,<<"carve">>}. |
| 3 | - {<<"version">>,<<"0.2.0">>}. |
| 3 | + {<<"version">>,<<"0.2.1">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<"DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.12">>}. |
| @@ -32,14 +32,15 @@ defmodule Carve.Links do | |
| 32 32 | |
| 33 33 | ## Examples |
| 34 34 | |
| 35 | - iex> Carve.Links.get_links_by_id(UserJSON, 1) |
| 36 | - [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}] |
| 35 | + iex> Carve.Links.get_links_by_id(UserJSON, 1) |
| 36 | + [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}] |
| 37 37 | |
| 38 | - iex> Carve.Links.get_links_by_id(UserJSON, [1, 2, 3], %{}, [:team]) |
| 39 | - [%{type: :team, id: "abc123", data: %{...}}] |
| 38 | + iex> Carve.Links.get_links_by_id(UserJSON, [1, 2, 3], %{}, [:team]) |
| 39 | + [%{type: :team, id: "abc123", data: %{...}}] |
| 40 40 | """ |
| 41 41 | def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil) |
| 42 42 | def get_links_by_id(_module, nil, _visited, _whitelist), do: [] |
| 43 | + |
| 43 44 | def get_links_by_id(module, id, visited, whitelist) when not is_list(id) do |
| 44 45 | case Map.get(visited, {module, id}) do |
| 45 46 | nil -> |
| @@ -47,9 +48,12 @@ defmodule Carve.Links do | |
| 47 48 | nil -> [] |
| 48 49 | data -> get_links_by_data(module, data, visited, whitelist) |> prepare_result(whitelist) |
| 49 50 | end |
| 50 | - _ -> [] |
| 51 | + |
| 52 | + _ -> |
| 53 | + [] |
| 51 54 | end |
| 52 55 | end |
| 56 | + |
| 53 57 | def get_links_by_id(module, ids, visited, whitelist) when is_list(ids) do |
| 54 58 | Enum.flat_map(ids, &get_links_by_id(module, &1, visited, whitelist)) |
| 55 59 | |> prepare_result(whitelist) |
| @@ -74,80 +78,88 @@ defmodule Carve.Links do | |
| 74 78 | |
| 75 79 | ## Examples |
| 76 80 | |
| 77 | - iex> user = %{id: 1, name: "John Doe", team_id: 2} |
| 78 | - iex> Carve.Links.get_links_by_data(UserJSON, user) |
| 79 | - [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}] |
| 81 | + iex> user = %{id: 1, name: "John Doe", team_id: 2} |
| 82 | + iex> Carve.Links.get_links_by_data(UserJSON, user) |
| 83 | + [%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}] |
| 80 84 | |
| 81 | - iex> user = %{id: 1, name: "John Doe", team_id: 2} |
| 82 | - iex> Carve.Links.get_links_by_data(UserJSON, user, %{}, [:team]) |
| 83 | - [%{type: :team, id: "abc123", data: %{...}}] |
| 85 | + iex> user = %{id: 1, name: "John Doe", team_id: 2} |
| 86 | + iex> Carve.Links.get_links_by_data(UserJSON, user, %{}, [:team]) |
| 87 | + [%{type: :team, id: "abc123", data: %{...}}] |
| 84 88 | """ |
| 89 | + |
| 90 | + # Get links for an entity by its data, with support for collections and filtering |
| 85 91 | def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil) |
| 92 | + |
| 93 | + # Return empty list for nil data |
| 86 94 | def get_links_by_data(_module, nil, _visited, _whitelist), do: [] |
| 95 | + |
| 96 | + # Handle collections by mapping over each item |
| 87 97 | def get_links_by_data(module, data_list, visited, whitelist) when is_list(data_list) do |
| 88 98 | Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist)) |
| 89 99 | |> prepare_result(whitelist) |
| 90 100 | end |
| 91 | - def get_links_by_data(_module, data, _visited, _whitelist) when not is_map(data) do |
| 92 | - [] |
| 93 | - end |
| 101 | + |
| 102 | + # Return empty list for non-map data |
| 103 | + def get_links_by_data(_module, data, _visited, _whitelist) when not is_map(data), do: [] |
| 104 | + |
| 105 | + # Process single entity's links, with circular reference prevention |
| 94 106 | def get_links_by_data(module, data, visited, whitelist) when not is_list(data) do |
| 95 107 | case fetch_id(data) do |
| 96 108 | {:ok, id} -> |
| 97 109 | if Map.get(visited, {module, id}) do |
| 98 110 | [] |
| 99 111 | else |
| 112 | + # Mark as visited and process links |
| 100 113 | visited = Map.put(visited, {module, id}, true) |
| 101 | - module.process_links(data) |
| 102 | - |> filter_links(whitelist) |
| 103 | - |> Enum.flat_map(fn {link_module, link_ids} -> |
| 104 | - Enum.map(normalize_link_ids(link_ids), fn link_id -> |
| 105 | - process_single_link(link_module, link_id, visited) |
| 106 | - end) |
| 114 | + |
| 115 | + # Get raw links and apply whitelist filtering/evaluation |
| 116 | + links = |
| 117 | + module.process_links(data) |
| 118 | + |> filter_and_evaluate_links(whitelist) |
| 119 | + |
| 120 | + links = |
| 121 | + links |
| 122 | + |> Enum.flat_map(fn {link_module, link_ids} -> |
| 123 | + link_ids |
| 124 | + |> normalize_link_ids() |
| 125 | + |> Enum.map(fn link_id -> |
| 126 | + process_single_link(link_module, link_id, visited) |
| 127 | + end) |
| 107 128 | end) |
| 108 | - |> prepare_result(whitelist) |
| 129 | + |
| 130 | + prepare_result(links, whitelist) |
| 109 131 | end |
| 110 | - :error -> [] |
| 132 | + |
| 133 | + :error -> |
| 134 | + [] |
| 111 135 | end |
| 112 136 | end |
| 113 137 | |
| 114 | - #def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, process_links_fn \\ nil) |
| 115 | - def get_links_by_data(_module, nil, _visited, _whitelist, _process_links_fn), do: [] |
| 116 | - |
| 117 | - def get_links_by_data(module, data_list, visited, whitelist, process_links_fn) when is_list(data_list) do |
| 118 | - Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist, process_links_fn)) |
| 119 | - |> prepare_result(whitelist) |
| 138 | + defp filter_and_evaluate_links(links, nil) do |
| 139 | + # Without whitelist - evaluate regular links only (no lazy) |
| 140 | + links |
| 141 | + |> Enum.filter(fn {_, value} -> not is_function(value) end) |
| 142 | + |> Enum.into(%{}) |
| 120 143 | end |
| 121 144 | |
| 122 | - def get_links_by_data(_module, data, _visited, _whitelist, _process_links_fn) when not is_map(data) do |
| 123 | - [] |
| 145 | + defp filter_and_evaluate_links(_links, []) do |
| 146 | + # Empty whitelist - evaluate nothing |
| 147 | + %{} |
| 124 148 | end |
| 125 149 | |
| 126 | - def get_links_by_data(module, data, visited, whitelist, process_links_fn) when not is_list(data) do |
| 127 | - case fetch_id(data) do |
| 128 | - {:ok, id} -> |
| 129 | - if Map.get(visited, {module, id}) do |
| 130 | - [] |
| 131 | - else |
| 132 | - visited = Map.put(visited, {module, id}, true) |
| 150 | + defp filter_and_evaluate_links(links, whitelist) when is_list(whitelist) do |
| 151 | + links |
| 152 | + |> Enum.filter(fn {module, _} -> module.type_name() in whitelist end) |
| 153 | + |> Enum.map(fn {module, value} -> |
| 154 | + evaluated = |
| 155 | + case value do |
| 156 | + fun when is_function(fun, 0) -> fun.() |
| 157 | + other -> other |
| 158 | + end |
| 133 159 | |
| 134 | - # Use provided process function or fall back to module's process_links |
| 135 | - links = case process_links_fn do |
| 136 | - nil -> module.process_links(data) |
| 137 | - func -> func.(data) |
| 138 | - end |
| 139 | - |
| 140 | - links |
| 141 | - |> filter_links(whitelist) |
| 142 | - |> Enum.flat_map(fn {link_module, link_ids} -> |
| 143 | - Enum.map(normalize_link_ids(link_ids), fn link_id -> |
| 144 | - process_single_link(link_module, link_id, visited) |
| 145 | - end) |
| 146 | - end) |
| 147 | - |> prepare_result(whitelist) |
| 148 | - end |
| 149 | - :error -> [] |
| 150 | - end |
| 160 | + {module, evaluated} |
| 161 | + end) |
| 162 | + |> Enum.into(%{}) |
| 151 163 | end |
| 152 164 | |
| 153 165 | @doc """ |
| @@ -164,20 +176,18 @@ defmodule Carve.Links do | |
| 164 176 | |
| 165 177 | ## Examples |
| 166 178 | |
| 167 | - iex> links = %{UserJSON => [1, 2], PostJSON => [3, 4]} |
| 168 | - iex> Carve.Links.filter_links(links, [:user]) |
| 169 | - %{UserJSON => [1, 2]} |
| 179 | + iex> links = %{UserJSON => [1, 2], PostJSON => [3, 4]} |
| 180 | + iex> Carve.Links.filter_links(links, [:user]) |
| 181 | + %{UserJSON => [1, 2]} |
| 170 182 | """ |
| 171 | - def filter_links(links, nil), do: links |
| 172 | - def filter_links(links, whitelist) do |
| 173 | - Enum.filter(links, fn {module, _} -> |
| 174 | - module.type_name() in whitelist |
| 175 | - end) |
| 183 | + defp filter_links(links, nil), do: links |
| 184 | + |
| 185 | + defp filter_links(links, whitelist) do |
| 186 | + links |
| 187 | + |> Enum.filter(fn {module, _} -> module.type_name() in whitelist end) |
| 176 188 | |> Enum.into(%{}) |
| 177 189 | end |
| 178 190 | |
| 179 | - # ... (rest of the module remains the same) |
| 180 | - |
| 181 191 | @doc """ |
| 182 192 | Prepares the final result set by flattening, removing nil values, eliminating duplicates, |
| 183 193 | and applying the whitelist filter if provided. |
| @@ -193,9 +203,9 @@ defmodule Carve.Links do | |
| 193 203 | |
| 194 204 | ## Examples |
| 195 205 | |
| 196 | - iex> result = [[%{type: :team, id: "abc"}, nil], [%{type: :profile, id: "def"}], %{type: :team, id: "abc"}] |
| 197 | - iex> Carve.Links.prepare_result(result, [:team]) |
| 198 | - [%{type: :team, id: "abc"}] |
| 206 | + iex> result = [[%{type: :team, id: "abc"}, nil], [%{type: :profile, id: "def"}], %{type: :team, id: "abc"}] |
| 207 | + iex> Carve.Links.prepare_result(result, [:team]) |
| 208 | + [%{type: :team, id: "abc"}] |
| 199 209 | """ |
| 200 210 | def prepare_result(result, whitelist \\ nil) do |
| 201 211 | result |
| @@ -205,7 +215,9 @@ defmodule Carve.Links do | |
| 205 215 | |> filter_result(whitelist) |
| 206 216 | end |
| 207 217 | |
| 218 | + defp filter_result(result, []), do: [] |
| 208 219 | defp filter_result(result, nil), do: result |
| 220 | + |
| 209 221 | defp filter_result(result, whitelist) do |
| 210 222 | Enum.filter(result, fn %{type: type} -> type in whitelist end) |
| 211 223 | end |
| @@ -230,22 +242,31 @@ defmodule Carve.Links do | |
| 230 242 | case Map.get(visited, {module, id}) do |
| 231 243 | nil -> |
| 232 244 | case module.get_by_id(id) do |
| 233 | - nil -> nil |
| 245 | + nil -> |
| 246 | + nil |
| 247 | + |
| 234 248 | data -> |
| 235 249 | module.prepare_for_view(data) |
| 236 250 | end |
| 237 | - _ -> nil |
| 251 | + |
| 252 | + _ -> |
| 253 | + nil |
| 238 254 | end |
| 239 255 | end |
| 256 | + |
| 240 257 | defp process_single_link(module, data, visited) do |
| 241 258 | case fetch_id(data) do |
| 242 259 | {:ok, id} -> |
| 243 260 | case Map.get(visited, {module, id}) do |
| 244 261 | nil -> |
| 245 262 | module.prepare_for_view(data) |
| 246 | - _ -> nil |
| 263 | + |
| 264 | + _ -> |
| 265 | + nil |
| 247 266 | end |
| 248 | - :error -> nil |
| 267 | + |
| 268 | + :error -> |
| 269 | + nil |
| 249 270 | end |
| 250 271 | end |
| 251 272 | |
| @@ -262,11 +283,11 @@ defmodule Carve.Links do | |
| 262 283 | |
| 263 284 | ## Examples |
| 264 285 | |
| 265 | - iex> Carve.Links.normalize_link_ids(1) |
| 266 | - [1] |
| 286 | + iex> Carve.Links.normalize_link_ids(1) |
| 287 | + [1] |
| 267 288 | |
| 268 | - iex> Carve.Links.normalize_link_ids([1, 2, 3]) |
| 269 | - [1, 2, 3] |
| 289 | + iex> Carve.Links.normalize_link_ids([1, 2, 3]) |
| 290 | + [1, 2, 3] |
| 270 291 | """ |
| 271 292 | defp normalize_link_ids(link_ids) when is_list(link_ids), do: link_ids |
| 272 293 | defp normalize_link_ids(link_id), do: [link_id] |
| @@ -286,22 +307,26 @@ defmodule Carve.Links do | |
| 286 307 | |
| 287 308 | ## Examples |
| 288 309 | |
| 289 | - iex> Carve.Links.fetch_id(%{id: 1}) |
| 290 | - {:ok, 1} |
| 310 | + iex> Carve.Links.fetch_id(%{id: 1}) |
| 311 | + {:ok, 1} |
| 291 312 | |
| 292 | - iex> Carve.Links.fetch_id(%{"id" => 2}) |
| 293 | - {:ok, 2} |
| 313 | + iex> Carve.Links.fetch_id(%{"id" => 2}) |
| 314 | + {:ok, 2} |
| 294 315 | |
| 295 | - iex> Carve.Links.fetch_id(%{foo: "bar"}) |
| 296 | - {:ok, {:foo, "bar"}} |
| 316 | + iex> Carve.Links.fetch_id(%{foo: "bar"}) |
| 317 | + {:ok, {:foo, "bar"}} |
| 297 318 | |
| 298 | - iex> Carve.Links.fetch_id("not a map") |
| 299 | - :error |
| 319 | + iex> Carve.Links.fetch_id("not a map") |
| 320 | + :error |
| 300 321 | """ |
| 301 322 | defp fetch_id(data) when is_map(data) do |
| 302 323 | cond do |
| 303 | - Map.has_key?(data, :id) -> {:ok, data.id} |
| 304 | - Map.has_key?(data, "id") -> {:ok, data["id"]} |
| 324 | + Map.has_key?(data, :id) -> |
| 325 | + {:ok, data.id} |
| 326 | + |
| 327 | + Map.has_key?(data, "id") -> |
| 328 | + {:ok, data["id"]} |
| 329 | + |
| 305 330 | true -> |
| 306 331 | case Enum.at(data, 0) do |
| 307 332 | {key, value} -> {:ok, {key, value}} |
| @@ -309,6 +334,7 @@ defmodule Carve.Links do | |
| 309 334 | end |
| 310 335 | end |
| 311 336 | end |
| 337 | + |
| 312 338 | defp fetch_id(id) when is_integer(id) or is_binary(id), do: {:ok, id} |
| 313 339 | defp fetch_id(_), do: :error |
| 314 340 | end |
| @@ -106,79 +106,54 @@ defmodule Carve.View do | |
| 106 106 | @doc false |
| 107 107 | defmacro __before_compile__(env) do |
| 108 108 | has_process_links = Module.defines?(env.module, {:process_links, 1}) |
| 109 | - has_lazy_process_links = Module.defines?(env.module, {:process_lazy_links, 2}) |
| 110 | - |
| 111 109 | quote do |
| 112 | - # New function: index with include parameter |
| 113 | - def index(%{result: data, include: include}) when is_list(data) do |
| 110 | + |
| 111 | + def index(%{result: data, include: include}) when is_list(data) do |
| 114 112 | results = Enum.map(data, &prepare_for_view/1) |
| 115 113 | links = if unquote(has_process_links) do |
| 116 114 | Carve.Links.get_links_by_data(__MODULE__, data, %{}, include) |
| 117 115 | else |
| 118 116 | [] |
| 119 117 | end |
| 120 | - |
| 121 | - lazy_links = if unquote(has_lazy_process_links) and include != nil do |
| 122 | - # For each data item, get its lazy links |
| 123 | - Enum.flat_map(data, fn item -> |
| 124 | - Enum.flat_map(include, fn type -> |
| 125 | - process_lazy_links(item, type) |
| 126 | - end) |
| 127 | - end) |
| 128 | - else |
| 129 | - [] |
| 130 | - end |
| 131 | - |
| 132 | - %{ |
| 133 | - result: results, |
| 134 | - links: links ++ lazy_links |
| 135 | - } |
| 136 | - end |
| 137 | - |
| 138 | - # New function: show with include parameter |
| 139 | - def show(%{result: data, include: include}) do |
| 140 | - result = prepare_for_view(data) |
| 141 | - |
| 142 | - links = if unquote(has_process_links) do |
| 143 | - Carve.Links.get_links_by_data(__MODULE__, data, %{}, include) |
| 144 | - else |
| 145 | - [] |
| 146 | - end |
| 147 | - |
| 148 | - lazy_links = if unquote(has_lazy_process_links) and include != nil do |
| 149 | - # Since data is a single item, just map over includes |
| 150 | - Enum.flat_map(include, fn type -> |
| 151 | - process_lazy_links(data, type) |
| 152 | - end) |
| 153 | - else |
| 154 | - [] |
| 155 | - end |
| 156 | - |
| 157 | - %{ |
| 158 | - result: result, |
| 159 | - links: links ++ lazy_links |
| 160 | - } |
| 161 | - end |
| 162 | - |
| 163 | - # Render a list of entities with their links |
| 164 | - def index(%{result: data}) when is_list(data) do |
| 165 | - results = Enum.map(data, &prepare_for_view/1) |
| 166 | - links = if unquote(has_process_links) do |
| 167 | - Carve.Links.get_links_by_data(__MODULE__, data) |
| 168 | - else |
| 169 | - [] |
| 170 | - end |
| 171 118 | %{ |
| 172 119 | result: results, |
| 173 120 | links: links |
| 174 121 | } |
| 175 122 | end |
| 176 123 | |
| 177 | - # Render a single entity with its links |
| 124 | + # Without include - include everything |
| 125 | + def index(%{result: data}) when is_list(data) do |
| 126 | + results = Enum.map(data, &prepare_for_view/1) |
| 127 | + links = if unquote(has_process_links) do |
| 128 | + Carve.Links.get_links_by_data(__MODULE__, data, %{}, nil) # Pass nil to include all |
| 129 | + else |
| 130 | + [] |
| 131 | + end |
| 132 | + %{ |
| 133 | + result: results, |
| 134 | + links: links |
| 135 | + } |
| 136 | + end |
| 137 | + |
| 138 | + # With include parameter - use whitelist |
| 139 | + def show(%{result: data, include: include}) do |
| 140 | + result = prepare_for_view(data) |
| 141 | + links = if unquote(has_process_links) do |
| 142 | + Carve.Links.get_links_by_data(__MODULE__, data, %{}, include) |
| 143 | + else |
| 144 | + [] |
| 145 | + end |
| 146 | + %{ |
| 147 | + result: result, |
| 148 | + links: links |
| 149 | + } |
| 150 | + end |
| 151 | + |
| 152 | + # Without include - include everything |
| 178 153 | def show(%{result: data}) do |
| 179 154 | result = prepare_for_view(data) |
| 180 155 | links = if unquote(has_process_links) do |
| 181 | - Carve.Links.get_links_by_data(__MODULE__, data) |
| 156 | + Carve.Links.get_links_by_data(__MODULE__, data, %{}, nil) # Pass nil to include all |
| 182 157 | else |
| 183 158 | [] |
| 184 159 | end |
| @@ -219,11 +194,6 @@ defmodule Carve.View do | |
| 219 194 | unless unquote(has_process_links) do |
| 220 195 | def process_links(_), do: %{} |
| 221 196 | end |
| 222 | - |
| 223 | - # Add default implementation for process_lazy_links |
| 224 | - unless unquote(has_lazy_process_links) do |
| 225 | - def process_lazy_links(_, _), do: [] |
| 226 | - end |
| 227 197 | end |
| 228 198 | end |
| 229 199 | |
| @@ -253,88 +223,6 @@ defmodule Carve.View do | |
| 253 223 | end |
| 254 224 | end |
| 255 225 | |
| 256 | - @doc """ |
| 257 | - Defines lazy-loaded links for the current view. |
| 258 | - |
| 259 | - This macro allows you to specify links that are only loaded when explicitly requested |
| 260 | - through the include parameter. Useful for expensive or optional relationships. |
| 261 | - |
| 262 | - ## Parameters |
| 263 | - |
| 264 | - - `func`: A function that takes an entity and returns a map of lazy link functions. |
| 265 | - Each function returns a tuple of {module, data_or_ids} when evaluated. |
| 266 | - |
| 267 | - ## Example |
| 268 | - |
| 269 | - lazy_links fn user -> |
| 270 | - %{ |
| 271 | - posts: fn -> {PostJSON, Posts.get_by_user(user.id)} end, |
| 272 | - comments: fn -> {CommentJSON, Comments.get_by_user(user.id)} end |
| 273 | - } |
| 274 | - end |
| 275 | - |
| 276 | - The lazy links can then be included in API responses: |
| 277 | - |
| 278 | - # Includes no lazy links |
| 279 | - GET /api/users/123 |
| 280 | - |
| 281 | - # Includes only posts |
| 282 | - GET /api/users/123?include=posts |
| 283 | - |
| 284 | - # Includes both posts and comments |
| 285 | - GET /api/users/123?include=posts,comments |
| 286 | - |
| 287 | - This creates a `process_lazy_links/2` function that handles: |
| 288 | - - Single IDs or structs |
| 289 | - - Lists of IDs or structs |
| 290 | - - Proper type/id/data formatting for consistency |
| 291 | - |
| 292 | - The returned links match the format of normal links: |
| 293 | - ```elixir |
| 294 | - %{ |
| 295 | - type: :post, |
| 296 | - id: "hashed_id", |
| 297 | - data: %{...} |
| 298 | - } |
| 299 | - ``` |
| 300 | - """ |
| 301 | - defmacro lazy_links(func) do |
| 302 | - quote do |
| 303 | - def process_lazy_links(data, type) do |
| 304 | - lazy_links = unquote(func).(data) |
| 305 | - case Map.get(lazy_links, type) do |
| 306 | - nil -> [] |
| 307 | - link_fn -> |
| 308 | - {module, items} = link_fn.() |
| 309 | - |
| 310 | - # Handle both single items and lists |
| 311 | - items_list = List.wrap(items) |
| 312 | - |
| 313 | - # Map each item to proper format |
| 314 | - Enum.map(items_list, fn item -> |
| 315 | - case item do |
| 316 | - # If it's just an ID |
| 317 | - id when is_integer(id) -> |
| 318 | - fetched = module.get_by_id(id) |
| 319 | - %{ |
| 320 | - type: module.type_name(), |
| 321 | - id: module.hash(id), |
| 322 | - data: module.prepare_for_view(fetched) |
| 323 | - } |
| 324 | - # If it's already a struct/map |
| 325 | - item -> |
| 326 | - %{ |
| 327 | - type: module.type_name(), |
| 328 | - id: module.hash(item.id), |
| 329 | - data: module.prepare_for_view(item) |
| 330 | - } |
| 331 | - end |
| 332 | - end) |
| 333 | - end |
| 334 | - end |
| 335 | - end |
| 336 | - end |
| 337 | - |
| 338 226 | @doc """ |
| 339 227 | Defines how to render the view for the current entity. |
| @@ -4,7 +4,7 @@ defmodule Carve.MixProject do | |
| 4 4 | def project do |
| 5 5 | [ |
| 6 6 | app: :carve, |
| 7 | - version: "0.2.0", |
| 7 | + version: "0.2.1", |
| 8 8 | elixir: "~> 1.12", |
| 9 9 | start_permanent: Mix.env() == :prod, |
| 10 10 | deps: deps(), |