Packages

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

Current section

12 Versions

Jump to

Compare versions

5 files changed
+142 additions
-44 deletions
  @@ -1,17 +1,30 @@
1 1 # Carve
2 2
3 - DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.
3 + Declare the view & relationships like this:
4 4
5 - Features:
5 + ```ex
6 + use Carve.View, :user
6 7
7 - * Rendering structured JSON views automatically
8 - * Retrieving links between different data types
9 - * Creating index/show methods for views
10 - * Hashing IDs with unique & short strings (e.g `123` -> `Xk9Lp2R`)
8 + view fn user ->
9 + %{
10 + id: hash(user.id),
11 + name: user.name
12 + }
13 + end
11 14
12 - Here's how an endpoint view generated by Carve looks like:
15 + links fn user ->
16 + %{
17 + TeamJSON => user.team_id,
18 + ProfileJSON => user.profile_id,
19 + PostsJSON => fn -> Posts.get_by_user_id(user.id) end
20 + }
21 + end
22 + ```
23 +
24 + Get JSON endpoint views (`index`, `show`) with complete & linked data back in single request:
13 25
14 26 ```js
27 + // GET /users/xyz?include=profile,team
15 28 {
16 29 "result": {
17 30 "id": "D3Wcorr0oa",
  @@ -33,6 +46,16 @@ Here's how an endpoint view generated by Carve looks like:
33 46 }
34 47 ```
35 48
49 + Features:
50 +
51 + * Automatic endpoint generation (index/show)
52 + * Smart data loading (lazy/eager)
53 + * Relationship resolution
54 + * Response structuring
55 + * ID hashing (123 → Xk9Lp2R)
56 +
57 + See also: [Bind](https://github.com/azer/bind)
58 +
36 59 ## Installation
37 60
38 61 Add `carve` to your list of dependencies in `mix.exs`:
  @@ -329,3 +352,46 @@ end
329 352 ## API
330 353
331 354 More detailed API docs are available at [https://hexdocs.pm/carve/Carve.html](https://hexdocs.pm/carve/Carve.html)
355 +
356 + ## Q&A
357 +
358 + ### Why?
359 +
360 + 1. **Performance vs Flexibility**
361 + - REST: Simple but requires multiple roundtrips
362 + - GraphQL: Flexible but complex infrastructure
363 + - Carve: Single request, zero infrastructure
364 +
365 + 2. **Development Speed**
366 + - Every new feature typically requires:
367 + - New endpoint handlers
368 + - New view logic
369 + - New data fetching code
370 + - New state management
371 + - Carve eliminates all of this with one view definition
372 +
373 + 3. **Maintainability**
374 + - As applications grow, data dependencies become complex
375 + - Changes ripple through multiple endpoints
376 + - State management becomes increasingly difficult
377 + - Carve centralizes this complexity in view definitions
378 +
379 + ### How does it compare to GraphQL?
380 +
381 + GraphQL
382 + - Requires schema definition (IDL) and resolver functions for each field
383 + - Client needs to learn query language and construct queries
384 + - Each query is a unique POST request, making caching challenging
385 + - N+1 query problems require manual batching/dataloader setup
386 + - Field-level authorization adds complexity
387 + - Additional infrastructure needed (query validation, complexity limits, persisted queries)
388 +
389 + Carve
390 + - Simple & small lbirary, uses your existing Phoenix views and controllers
391 + - Compatible with other libraries like [Bind](https://github.com/azer/bind)
392 + - REST-like URLs with simple `?include=` parameter
393 + - Standard GET requests, works with HTTP caching out of the box
394 + - Automatic batching of related data queries
395 + - Resource-level authorization using your existing Phoenix plugs
396 + - Zero additional infrastructure required
397 +
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/azer/carve">>}]}.
2 2 {<<"name">>,<<"carve">>}.
3 - {<<"version">>,<<"0.2.1">>}.
3 + {<<"version">>,<<"0.3.0">>}.
4 4 {<<"description">>,
5 5 <<"DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.">>}.
6 6 {<<"elixir">>,<<"~> 1.12">>}.
  @@ -114,7 +114,7 @@ defmodule Carve.Links do
114 114
115 115 # Get raw links and apply whitelist filtering/evaluation
116 116 links =
117 - module.process_links(data)
117 + module.declare_links(data)
118 118 |> filter_and_evaluate_links(whitelist)
119 119
120 120 links =
  @@ -123,7 +123,18 @@ defmodule Carve.Links do
123 123 link_ids
124 124 |> normalize_link_ids()
125 125 |> Enum.map(fn link_id ->
126 - process_single_link(link_module, link_id, visited)
126 + link = process_single_link(link_module, link_id, visited)
127 + children = if Map.get(visited, {link_module, link_id}) do
128 + []
129 + else
130 + if is_map(link_id) do
131 + get_links_by_data(link_module, link_id, visited, whitelist)
132 + else
133 + get_links_by_id(link_module, link_id, visited, whitelist)
134 + end
135 + end
136 +
137 + [link | children]
127 138 end)
128 139 end)
  @@ -1,5 +1,5 @@
1 1 defmodule Carve.View do
2 - @moduledoc """
2 + @moduledoc """
3 3 Carve.View provides a DSL for quickly building JSON API views in Phoenix applications.
4 4
5 5 It automatically creates `show` and `index` functions for Phoenix controllers,
  @@ -46,7 +46,7 @@ defmodule Carve.View do
46 46 - `encode_id/1`: Encodes an ID using the view type as a salt.
47 47 - `decode_id/1`: Decodes an ID using the view type as a salt.
48 48 - `type_name/0`: Returns the type of the view.
49 - - `process_links/1`: Processes links for an entity (if `links` macro is used).
49 + - `declare_links/1`: Processes links for an entity (if `links` macro is used).
50 50
51 51 Here's an example of what these functions might look like at runtime:
52 52
  @@ -67,7 +67,7 @@ defmodule Carve.View do
67 67
68 68 def type_name, do: :user
69 69
70 - def process_links(data) do
70 + def declare_links(data) do
71 71 %{
72 72 MyApp.TeamJSON => data.team_id,
73 73 MyApp.ProfileJSON => data.profile_id
  @@ -105,16 +105,19 @@ defmodule Carve.View do
105 105
106 106 @doc false
107 107 defmacro __before_compile__(env) do
108 - has_process_links = Module.defines?(env.module, {:process_links, 1})
109 - quote do
108 + has_declare_links = Module.defines?(env.module, {:declare_links, 1})
110 109
111 - def index(%{result: data, include: include}) when is_list(data) do
110 + quote do
111 + def index(%{result: data, include: include}) when is_list(data) do
112 112 results = Enum.map(data, &prepare_for_view/1)
113 - links = if unquote(has_process_links) do
114 - Carve.Links.get_links_by_data(__MODULE__, data, %{}, include)
115 - else
116 - []
117 - end
113 +
114 + links =
115 + if unquote(has_declare_links) do
116 + Carve.Links.get_links_by_data(__MODULE__, data, %{}, include)
117 + else
118 + []
119 + end
120 +
118 121 %{
119 122 result: results,
120 123 links: links
  @@ -124,11 +127,15 @@ defmodule Carve.View do
124 127 # Without include - include everything
125 128 def index(%{result: data}) when is_list(data) do
126 129 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
130 +
131 + links =
132 + if unquote(has_declare_links) do
133 + # Pass nil to include all
134 + Carve.Links.get_links_by_data(__MODULE__, data, %{}, nil)
135 + else
136 + []
137 + end
138 +
132 139 %{
133 140 result: results,
134 141 links: links
  @@ -138,11 +145,14 @@ defmodule Carve.View do
138 145 # With include parameter - use whitelist
139 146 def show(%{result: data, include: include}) do
140 147 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
148 +
149 + links =
150 + if unquote(has_declare_links) do
151 + Carve.Links.get_links_by_data(__MODULE__, data, %{}, include)
152 + else
153 + []
154 + end
155 +
146 156 %{
147 157 result: result,
148 158 links: links
  @@ -152,11 +162,15 @@ defmodule Carve.View do
152 162 # Without include - include everything
153 163 def show(%{result: data}) do
154 164 result = prepare_for_view(data)
155 - links = if unquote(has_process_links) do
156 - Carve.Links.get_links_by_data(__MODULE__, data, %{}, nil) # Pass nil to include all
157 - else
158 - []
159 - end
165 +
166 + links =
167 + if unquote(has_declare_links) do
168 + # Pass nil to include all
169 + Carve.Links.get_links_by_data(__MODULE__, data, %{}, nil)
170 + else
171 + []
172 + end
173 +
160 174 %{
161 175 result: result,
162 176 links: links
  @@ -178,8 +192,7 @@ defmodule Carve.View do
178 192 Carve.HashIds.encode(@carve_type, id)
179 193 end
180 194
181 -
182 - # Decode a hashed ID
195 + # Decode a hashed ID
183 196 def decode_id(hashed_id) when is_binary(hashed_id) do
184 197 case Carve.HashIds.decode(@carve_type, hashed_id) do
185 198 {:ok, id} -> {:ok, id}
  @@ -187,12 +200,19 @@ defmodule Carve.View do
187 200 end
188 201 end
189 202
203 + def decode_id!(hashed_id) when is_binary(hashed_id) do
204 + case decode_id(hashed_id) do
205 + {:ok, id} -> id
206 + {:error, reason} -> raise "Failed to decode ID: #{reason}"
207 + end
208 + end
209 +
190 210 # Return the type of this view
191 211 def type_name, do: @carve_type
192 212
193 - # Default process_links function if not defined by user
194 - unless unquote(has_process_links) do
195 - def process_links(_), do: %{}
213 + # Default declare_links function if not defined by user
214 + unless unquote(has_declare_links) do
215 + def declare_links(_), do: %{}
196 216 end
197 217 end
198 218 end
  @@ -217,7 +237,7 @@ defmodule Carve.View do
217 237 """
218 238 defmacro links(func) do
219 239 quote do
220 - def process_links(data) do
240 + def declare_links(data) do
221 241 unquote(func).(data)
222 242 end
223 243 end
  @@ -246,6 +266,7 @@ defmodule Carve.View do
246 266 quote do
247 267 def prepare_for_view(data) do
248 268 view = unquote(func).(data)
269 +
249 270 %{
250 271 id: hash(data.id),
251 272 type: type_name(),
  @@ -4,7 +4,7 @@ defmodule Carve.MixProject do
4 4 def project do
5 5 [
6 6 app: :carve,
7 - version: "0.2.1",
7 + version: "0.3.0",
8 8 elixir: "~> 1.12",
9 9 start_permanent: Mix.env() == :prod,
10 10 deps: deps(),
  @@ -52,7 +52,7 @@ defmodule Carve.MixProject do
52 52
53 53 defp package do
54 54 [
55 - maintainers: ["Your Name"],
55 + maintainers: ["Azer Koculu"],
56 56 licenses: ["MIT"],
57 57 links: %{"GitHub" => "https://github.com/azer/carve"}
58 58 ]