/solage

Basic tools to implement a JSON API with awesome features taken from the jsonapi.org spec

Primary LanguageElixirMIT LicenseMIT

Travis

Provides basic functionalities to implement a JSON API-compliant API.

Installation

Add solage to the deps function in your project’s mix.exs file:

defp deps do
  [,
    {:solage, "~> 0.1"}
  ]
end

Then run mix do deps.get, deps.compile inside your project’s directory.

Why?

While implementing a JSON API-compliant API in Elixir, I stumbled upon two great packages: JaSerializer and jsonapi. With great respect to their maintainers, I found three problems with those packages:

  • Restrictive and unextensible Domain-Specific-Language
  • URL guessing
  • Missing query restriction, or restriction at the wrong place (Which relation can be included, which filters are permitted)

At first I tried to contribute to the packages but I thought that it could be a good exercise to implement those things myself :P A lot of what is implemented comes directly from those two packages. Thanks again for the cleverness of their maintainers.

Description

This package doesn’t implement the whole view with id, type, attributes or relationships. It lets your application define this stuff. It’s a bit more of boilerplate code to write but you’ll never have to bend or hook you in a restrictive DSL. This means you can implement an API with nested resources and not use the included feature. You would benefit from awesome features such as preload parsing, sort parsing, etc. Those concepts are good on their own without attaching the whole JSON API specification in front of it. This is what this package is aiming for.

  • Don’t want to provide unecessary routes? This package won’t invent routes for your app.
  • Want to implement sparse fields on a certain view? With the render/3 function, you have all the tools to implement this kind of thing.
  • Want to embed a certain assocation on the object data? You get the point.

Solage.QueryConfig

By using the QueryConfig, you don’t have to hardcode data behaviour right in the view. It’s up to who is rendering the view to use a config that fits your need. The config can be initialized with the Solage.Query plug.

What’s inside QueryConfig

  • include: List all includes required by the request. With options set at the plug level, you can add always_includes or whitelist includes with allowed_includes. The output is compliant to a MyApp.Repo.preload/2 function call (easy integration with Ecto!). The include is used to build the included structure found in a JSON API response.
  • sort: List that looks like [asc: :attribute, desc: :other_attribute]. Like the include option, you can specify options at the plug level to allow specific sort params.
  • filter: Map that can be filtered like the above options.
  • fields: Map that contains fields that need to be in the resource data.

Solage.Serializer

Use the Serializer to render the data and included section of the JSON API. You only need a view that implements render/3 and you’re ready to go. To obtain the relation view of an included resource, you will need to implement relation_view/1 that receive the relation name as an atom.

What’s inside Serializer

2 functions

  • render/4: Render the representation of a resource. It’s just a proxy for view.render(data, query_config, conn)
  • included/4: With the QueryConfig, parse the data and call data/4 on each resource. Takes all the rendered resources and put them in a list.

Solage.DataTransform

The DataTransform module provides a quick and extensible way of encoding and decoding data keys. You could use it in a plug before using the params to replace - with _ on all keys. Or run it on the encoded data before sending the response.

The decoder and encoder are set in the config :solage, :decoder_formatter and :solage, :encoder_formatter. Check the Solage.DataTransform.Transformer behaviour to implement your own.

Valid formatters

  • :dasherized (Default)
  • :nothing Don’t transform the keys
  • Any module that respond to decode_key/1 and encode_key/1

Examples

plug :deserialize_params

defp deserialize_params(conn, _) do
  %{conn | params: Solage.DataTransform.decode(conn.params)}
end

Solage.AttributeTransform

The AttributeTransform module provides a clean and extensible way to transform a JSON API payload to a usable flat map like a regular REST API would accept. You can use the default resolver that assume that your relationship will be the name of the association + _id for belongs_to associations and name of the association + _ids for has_many associations.

But the interface could be implemented to support other kind of input. Check the Solage.AttributeTransform.Transformer behaviour to implement your own.

Examples

params = {
  "type": "person",
  "attributes": {"first": "Jane", "last": "Doe", "type": "anon"},
  "relationships": {"user": {"data": {"id": 1}}}
}

Solage.AttributeTransform.flatten(params)
# => {
#      "person": {
#        "first": "Jane",
#        "last": "Doe",
#        "type": "anon",
#        "user_id": 1
#      }
#    }

Examples

Views

defmodule PostView do
  def render(data, _config, _conn) do
    %{
      id: data["id"],
      relationships: %{
        user: data["author_id"]
      }
    }
  end

  def relation_view(:author), do: UserView
end

defmodule UserView do
  def render(data, _config, _conn) do
    %{
      id: data["id"],
      full_name: data["fullname"]
    }
  end
end

Endpoint

def show(conn, _) do
  # Transform params key with the right format
  conn = %{conn | params: Solage.DataTransform.decode(conn.params)}

  # Render the data value
  data = Solage.Serializer.render(PostView, conn.assigns[:post], conn.assigns[:jsonapi_query], conn)

  # Render the included value
  included = Solage.Serializer.included(PostView, conn.assigns[:post], conn.assigns[:jsonapi_query], conn)

  # Encode the final body with the right format
  body = Solage.DataTransform.encode(%{data: data, included: included})

  # Send the json response
  json(conn, 200, body)
end

Reponse

{
  "included": [
    {
      "id": "my-author-id",
      "full-name": "Testy"
    }
  ],
  "data": [
    {
      "id": "my-post-id",
      "relationships": {
        "author": "my-author-id"
      }
    }
  ]
}

License

Solage is © 2016 Simon Prévost and may be freely distributed under the MIT license. See the LICENSE.md file for more information.