Packages

DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.

Current section

12 Versions

Jump to

Compare versions

8 files changed
+339 additions
-331 deletions
  @@ -340,6 +340,45 @@ links fn user ->
340 340 end
341 341 ```
342 342
343 + ## Preload support
344 +
345 + Return preloaded association in your views, falling back to id using `data_or_id` macro:
346 +
347 + ```elixir
348 + links fn image ->
349 + %{
350 + UserJSON => data_or_id(image.user, image.user_id),
351 + }
352 + end
353 + ```
354 +
355 + Example controller:
356 +
357 + ```elixir
358 + def index(conn, params) do
359 + assets =
360 + MyApp.Assets.Asset
361 + |> MyApp.Assets.scope_to_user(conn.assigns.current_user.id)
362 + |> MyApp.Assets.scope_active()
363 + |> MyApp.Repo.all()
364 + |> MyApp.Repo.preload([:current_version, user: :subscription])
365 +
366 + render(conn, :index, %{result: assets, include: Carve.parse_include(params)})
367 + end
368 + ```
369 +
370 + ## Caching
371 +
372 + Carve includes request-scoped caching enabled by default, using [cachex](https://github.com/whitfin/cachex) under the hood. Each linked resource is stored in the cache for 100ms by default.
373 +
374 + To disable caching:
375 +
376 + ```
377 + ```elixir
378 + config :carve, Carve.Config,
379 + enable_cache: false
380 + ```
381 +
343 382 ## How does it work?
344 383
345 384 * Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })`
  @@ -394,4 +433,3 @@ Carve
394 433 - Automatic batching of related data queries
395 434 - Resource-level authorization using your existing Phoenix plugs
396 435 - Zero additional infrastructure required
397 -
  @@ -1,29 +1,34 @@
1 - {<<"links">>,[{<<"GitHub">>,<<"https://github.com/azer/carve">>}]}.
2 - {<<"name">>,<<"carve">>}.
3 - {<<"version">>,<<"0.3.0">>}.
1 + {<<"app">>,<<"carve">>}.
2 + {<<"build_tools">>,[<<"mix">>]}.
4 3 {<<"description">>,
5 4 <<"DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.">>}.
6 5 {<<"elixir">>,<<"~> 1.12">>}.
7 - {<<"app">>,<<"carve">>}.
8 - {<<"licenses">>,[<<"MIT">>]}.
9 - {<<"requirements">>,
10 - [[{<<"name">>,<<"phoenix">>},
11 - {<<"app">>,<<"phoenix">>},
12 - {<<"optional">>,true},
13 - {<<"requirement">>,<<"~> 1.6">>},
14 - {<<"repository">>,<<"hexpm">>}],
15 - [{<<"name">>,<<"jason">>},
16 - {<<"app">>,<<"jason">>},
17 - {<<"optional">>,false},
18 - {<<"requirement">>,<<"~> 1.2">>},
19 - {<<"repository">>,<<"hexpm">>}],
20 - [{<<"name">>,<<"hashids">>},
21 - {<<"app">>,<<"hashids">>},
22 - {<<"optional">>,false},
23 - {<<"requirement">>,<<"~> 2.1">>},
24 - {<<"repository">>,<<"hexpm">>}]]}.
25 6 {<<"files">>,
26 - [<<"lib">>,<<"lib/links.ex">>,<<"lib/config.ex">>,<<"lib/carve.ex">>,
27 - <<"lib/view.ex">>,<<"lib/hashids.ex">>,<<".formatter.exs">>,<<"mix.exs">>,
28 - <<"README.md">>]}.
29 - {<<"build_tools">>,[<<"mix">>]}.
7 + [<<"lib">>,<<"lib/links.ex">>,<<"lib/cache.ex">>,<<"lib/config.ex">>,
8 + <<"lib/carve.ex">>,<<"lib/view.ex">>,<<"lib/hashids.ex">>,
9 + <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>]}.
10 + {<<"licenses">>,[<<"MIT">>]}.
11 + {<<"links">>,[{<<"GitHub">>,<<"https://github.com/azer/carve">>}]}.
12 + {<<"name">>,<<"carve">>}.
13 + {<<"requirements">>,
14 + [[{<<"app">>,<<"phoenix">>},
15 + {<<"name">>,<<"phoenix">>},
16 + {<<"optional">>,true},
17 + {<<"repository">>,<<"hexpm">>},
18 + {<<"requirement">>,<<"~> 1.6">>}],
19 + [{<<"app">>,<<"jason">>},
20 + {<<"name">>,<<"jason">>},
21 + {<<"optional">>,false},
22 + {<<"repository">>,<<"hexpm">>},
23 + {<<"requirement">>,<<"~> 1.2">>}],
24 + [{<<"app">>,<<"hashids">>},
25 + {<<"name">>,<<"hashids">>},
26 + {<<"optional">>,false},
27 + {<<"repository">>,<<"hexpm">>},
28 + {<<"requirement">>,<<"~> 2.1">>}],
29 + [{<<"app">>,<<"cachex">>},
30 + {<<"name">>,<<"cachex">>},
31 + {<<"optional">>,false},
32 + {<<"repository">>,<<"hexpm">>},
33 + {<<"requirement">>,<<"~> 3.6">>}]]}.
34 + {<<"version">>,<<"0.4.0">>}.
  @@ -0,0 +1,89 @@
1 + # lib/cache.ex - Add enabled check
2 + defmodule Carve.Cache do
3 + @moduledoc """
4 + Request-scoped cache for Carve to prevent redundant data fetching within a single render.
5 + Each render operation gets its own isolated cache that expires after 100ms.
6 +
7 + Can be disabled via config:
8 +
9 + config :carve, Carve.Config,
10 + enable_cache: false
11 + """
12 +
13 + @ttl 100
14 +
15 + @doc """
16 + Creates a new cache context for a render operation.
17 + Returns a cache key that should be passed through the render pipeline.
18 + """
19 + def new_context do
20 + if Carve.Config.caching_enabled?() do
21 + cache_key = make_ref()
22 + Cachex.put(:carve_cache, cache_key, %{}, ttl: @ttl)
23 + cache_key
24 + else
25 + nil
26 + end
27 + end
28 +
29 + @doc """
30 + Fetches a value from cache or executes the function if not cached.
31 + If caching is disabled, always executes the function.
32 + """
33 + def fetch(nil, _key, fun) when is_function(fun, 0), do: fun.()
34 +
35 + def fetch(cache_key, key, fun) when is_function(fun, 0) do
36 + if Carve.Config.caching_enabled?() do
37 + case Cachex.get(:carve_cache, cache_key) do
38 + {:ok, nil} ->
39 + fun.()
40 +
41 + {:ok, cache_data} ->
42 + case Map.get(cache_data, key) do
43 + nil ->
44 + value = fun.()
45 + updated_cache = Map.put(cache_data, key, value)
46 + Cachex.put(:carve_cache, cache_key, updated_cache, ttl: @ttl)
47 + value
48 +
49 + cached_value ->
50 + cached_value
51 + end
52 +
53 + {:error, _} ->
54 + fun.()
55 + end
56 + else
57 + fun.()
58 + end
59 + end
60 +
61 + @doc """
62 + Gets the current cache key from the process dictionary, or creates a new one.
63 + Returns nil if caching is disabled.
64 + """
65 + def get_or_create_context do
66 + if Carve.Config.caching_enabled?() do
67 + case Process.get(:carve_cache_key) do
68 + nil ->
69 + cache_key = new_context()
70 + Process.put(:carve_cache_key, cache_key)
71 + cache_key
72 +
73 + cache_key ->
74 + cache_key
75 + end
76 + else
77 + nil
78 + end
79 + end
80 +
81 + @doc """
82 + Clears the cache context from the process dictionary.
83 + """
84 + def clear_context do
85 + if Carve.Config.caching_enabled?() do
86 + Process.delete(:carve_cache_key)
87 + end
88 + end
89 + end
  @@ -25,8 +25,17 @@ defmodule Carve do
25 25 """
26 26 def start(_type, _args) do
27 27 Logger.info("Starting Carve")
28 +
29 + # Start Cachex for request-scoped caching
30 + children = [
31 + {Cachex, name: :carve_cache}
32 + ]
33 +
34 + opts = [strategy: :one_for_one, name: Carve.Supervisor]
35 + result = Supervisor.start_link(children, opts)
36 +
28 37 configure(Carve.Config.get())
29 - {:ok, self()}
38 + result
30 39 end
31 40
32 41 @doc """
  @@ -85,12 +94,16 @@ defmodule Carve do
85 94 cond do
86 95 is_integer(data_or_ids) ->
87 96 Carve.Links.get_links_by_id(module, data_or_ids)
97 +
88 98 is_list(data_or_ids) and Enum.all?(data_or_ids, &is_integer/1) ->
89 99 Carve.Links.get_links_by_id(module, data_or_ids)
100 +
90 101 is_map(data_or_ids) ->
91 102 Carve.Links.get_links_by_data(module, data_or_ids)
103 +
92 104 is_list(data_or_ids) and Enum.all?(data_or_ids, &is_map/1) ->
93 105 Carve.Links.get_links_by_data(module, data_or_ids)
106 +
94 107 true ->
95 108 []
96 109 end
  @@ -192,14 +205,18 @@ defmodule Carve do
192 205 def parse_include(params) do
193 206 if include_param_specified?(params) do
194 207 case params["include"] do
195 - nil -> nil
196 - "" -> []
208 + nil ->
209 + nil
210 +
211 + "" ->
212 + []
213 +
197 214 string when is_binary(string) ->
198 215 string
199 216 |> String.split(",", trim: true)
200 217 |> Enum.map(&String.trim/1)
201 218 |> Enum.reject(&(&1 == ""))
202 - |> Enum.uniq()
219 + |> Enum.uniq()
203 220 |> Enum.map(&String.to_existing_atom/1)
204 221 end
205 222 else
  @@ -1,3 +1,4 @@
1 + # lib/config.ex - Add caching configuration
1 2 defmodule Carve.Config do
2 3 @moduledoc """
3 4 Handles configuration for Carve.
  @@ -17,4 +18,11 @@ defmodule Carve.Config do
17 18 get()
18 19 |> Keyword.get(key, default)
19 20 end
21 +
22 + @doc """
23 + Checks if caching is enabled. Defaults to true.
24 + """
25 + def caching_enabled? do
26 + get(:enable_cache, true)
27 + end
20 28 end
Loading more files…