README.md

# Carve

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

Features:

* Rendering structured JSON views automatically
* Retrieving links between different data types
* Creating index/show methods for views
* Hashing IDs with unique & short strings (e.g `123` -> `Xk9Lp2R`)

Here's how an endpoint view generated by Carve looks like:

```js
{
  "result": {
    "id": "D3Wcorr0oa",
    "type": "user",
    "data": { ... }
  },
  "links": [
    {
      "id": "Xk9Lp2Rr4m",
      "type": "team",
      "data": { ... }
    },
    {
      "id": "Bz7Jt5Yq1n",
      "type": "profile",
      "data": { ... }
    }
  ]
}
```

## Installation

Add `carve` to your list of dependencies in `mix.exs`:


```elixir
def deps do
  [
    {:carve, "~> 0.1.0"}
  ]
end
```


To configure custom hashing salt, add to `config.exs`:

```elixir
config :carve,
  salt: "your-secret-salt",
  min_length: 10
```

## Usage

In your Phoenix JSON view, just declare how your endpoint should look like:

```elixir
defmodule UserJSON do
    use Carve.View, :user

    view fn user ->
      %{
        id: hash(user.id), # provided by Carve.View, encodes numerical ID (123) with an entity-specific salt (D3Wcorr0oa).
        name: user.name
      }
    end
end
```

And that's it -- this macro will make `index(%{ result: users })` and `show(%{ result: user })` methods available for your controller.

Of course, just formatting the view alone is not Carvers only point; you can declare the links of each view, and Carve will automatically output them for the client:

```elixir
defmodule UserJSON do
    use Carve.View, :user

    # Return the links of a given user as ViewModule => id
    links fn user ->
        %{
            FooWeb.TeamJSON => user.team_id, # You can also pass list of ids or the Ecto record(s)
            FooWeb. ProfileJSON => user.profile_id
        }
    end

    # Provide a method to retrieve user by id. This will be used by Carve to render links automatically.
    get fn id ->
        Foo.Users.get_by_id!(id)
    end

    view fn user ->
      %{
        id: hash(user.id),
        team_id: FooWeb.TeamJSON.hash(user.team_id),
        profile_id: FooWeb.ProfileJSON.hash(user.profile_id),
        name: user.name
      }
    end
end
```

After activating `show` and `index` methods in the controller:

```elixir
render(conn, :index, result: requests) # or render(conn, :show, result: record)
```

Example response Carve will render for this view:

```json
{
  "result": {
    "id": "D3Wcorr0oa",
    "type": "user",
    "data": {
      "id": "D3Wcorr0oa",
      "name": "John Doe",
      "team_id": "Xk9Lp2Rr4m",
      "profile_id": "Bz7Jt5Yq1n"
    }
  },
  "links": [
    {
      "id": "Xk9Lp2Rr4m",
      "type": "team",
      "data": {
        "id": "Xk9Lp2Rr4m",
        "name": "Engineering Team",
        "description": "Our awesome engineering team"
      }
    },
    {
      "id": "Bz7Jt5Yq1n",
      "type": "profile",
      "data": {
        "id": "Bz7Jt5Yq1n",
        "bio": "Software engineer passionate about Elixir",
        "avatar_url": "https://example.com/avatars/johndoe.jpg"
      }
    }
  ]
}
```

### Example Controller

Carve provides a flexible way to control which linked data is included in the response. This is achieved through the include parameter. Here's an example of how to use Carve in your Phoenix controllers:

```elixir
defmodule UserController do
  use FooWeb, :controller

  def show(conn, %{"id" => id} = params) do
    user = Foo.Users.get_user!(id)
    include = Carve.parse_include(params)

    render(conn, :show, %{ result: user, include: include })
  end

  def index(conn, params) do
    users = Foo.Users.list_users()
    include = Carve.parse_include(params)

    render(conn, :index, %{ result: users, include: include })
  end
end
```

This example also shows reading & parsing the `include` parameter, which can be one of following:

* Not specified (`GET  /api/users`): All link types are included.
* Empty list (`GET /api/users?included=`): No link types are included.
* Custom types: (`GET /api/users/123?include=team,profile`): Include comma-separated link types only.

## How does it work?

* Carve macros create view functions `index(%{ result: users })` and `show(%{ result: user })`
* Controller calls these view functions
* Carve pulls the list of links for given data (list or single record)
* Carve calls the `get_by_id` (`get` macro expanded) and `prepare_for_view` (`view` macro expanded) functions for each link
* The final expanded list of links get flattened & cleaned, returned to user with the main result: `{ result: {} || [], links: [] }`

## Links

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:

```elixir
defmodule UserJSON do
  use Carve.View, :user

  links fn user ->
    %{
      TeamJSON => user.team_id,
      CompanyJSON => user.company_id
    }
  end
end
```

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.



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:

```elixir
defmodule UserJSON do
  use Carve.View, :user

  # Declare which other resources this view links to
  links fn user ->
    %{
      TeamJSON => user.team_id,       # User's team - needed to render user profile
      CompanyJSON => user.company_id  # User's company - needed for permissions
    }
  end
end
```

### Include Parameter

Carve allows selective loading so the API client can optimize the response size & number of DB queries.

You can enable it in the controller:

```ex
def show(conn, params) do
  user = Users.get!(params["id"])

  # Parses ?include=team,post into [:team, :post]
  include = Carve.parse_include(params)

  render(conn, :show, %{
    result: user,
    include: include
  })
end
```

The client now can specify what links should be included in the API.

```
GET /api/users/123                    # Include all links
GET /api/users/123?include=           # Include no links
GET /api/users/123?include=team       # Include only team links
GET /api/users/123?include=team,post  # Include team and post links
```

Example response with `?include=team`:

```json
{
  "result": {
    "id": "D3Wcorr0oa",
    "type": "user",
    "data": {
      "id": "D3Wcorr0oa",
      "name": "John Doe",
      "team_id": "Xk9Lp2Rr4m"
    }
  },
  "links": [
    {
      "id": "Xk9Lp2Rr4m",
      "type": "team",
      "data": {
        "id": "Xk9Lp2Rr4m",
        "name": "Engineering Team"
      }
    }
  ]
}
```

## Lazy Links

When using `links`, even if a link type is filtered out via `?include=`, database queries are still executed for all linked resources.

For expensive queries, you can use `lazy_links` to execute them only when explicitly requested:

```elixir
defmodule MyJSON do
  use Carve.View, :resource

  lazy_links fn resource ->
    %{
      comments: fn -> {CommentJSON, Comments.by_user_id(user.id)} end
    }
  end
end
```

The lazy function is evaluated only when requested in the include param:

```
GET /api/users/123                # No comments query executed
GET /api/users/123?include=comments  # Query executed for fetching comments
```

Both return same JSON format, just with different loading behavior. The function should return a tuple of `{ViewModule, id_or_ids}`.

### Large datasets

For relationships that could return large datasets, create dedicated endpoints instead of links:

```elixir
#  ✅ Good: /api/users/123 returns user with essential context
links fn user ->
  %{TeamJSON => user.team_id}
end

# ✅ Good: Get user's comments via dedicated endpoint
GET /api/users/123/comments?page=1

# ❌ Bad: Don't use links for large collections
links fn user ->
  %{
    CommentsJSON => Comments.by_user_id(user.id)  # Could be thousands
  }
end
```

## API

More detailed API docs are available at [https://hexdocs.pm/carve/Carve.html](https://hexdocs.pm/carve/Carve.html)