Current section
12 Versions
Jump to
Current section
12 Versions
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…