vt-elixir / ja_serializer

JSONAPI.org Serialization in Elixir.
Other
640 stars 148 forks source link
elixir json-api

JaSerializer

Build Status Hex Version Inline docs

jsonapi.org formatting of Elixir data structures suitable for serialization by libraries such as Poison.

Usage

See documentation on hexdoc for full serialization and usage details.

Installation

Add JaSerializer to your application

mix.deps

defp deps do
  [
    # ...
      {:ja_serializer, "~> x.x.x"}
    # ...
  ]
end

Serializer Behaviour and DSL

defmodule MyApp.ArticleSerializer do
  use JaSerializer

  location "/articles/:id"
  attributes [:title, :tags, :body, :excerpt]

  has_one :author,
    serializer: PersonSerializer,
    include: true,
    field: :authored_by

  has_many :comments,
    links: [
      related: "/articles/:id/comments",
      self: "/articles/:id/relationships/comments"
    ]

  def comments(article, _conn) do
    Comment.for_article(article)
  end

  def excerpt(article, _conn) do
    [first | _ ] = String.split(article.body, ".")
    first
  end
end

Attributes

Attributes are defined as a list in the serializer module. The serializer will use the given atom as the key by default. You can also specify a custom method of attribute retrieval by defining a

/2 method. The method will be passed the struct and the connection. ### Relationships Valid relationships are: `has_one`, `has_many`. Use `has_one` for `belongs_to` type of relationships. For each relationship, you can define the name and a variety of options. Just like attributes, the serializer will use the given atom to look up the relationship, unless you specify a custom retrieval method OR provide a `field` option #### Relationship options * serializer - The serializer to use when serializing this resource * include - boolean - true to always side-load this relationship * field - custom field to use for relationship retrieval * links - custom links to use in the `relationships` hash ### Direct Usage of Serializer ```elixir MyApp.ArticleSerializer |> JaSerializer.format(struct, conn) |> Poison.encode! ``` ### Formatting options The `format/4` method is able to take in options that can customize the serialized payload. #### Include By specifying the `include` option, the serializer will only side-load the relationships specified. This option should be a comma separated list of relationships. Each relationship should be a dot separated path. Example: `include: "author,comments.author"` The format of this string should exactly match the one specified by the [JSON-API spec](http://jsonapi.org/format/#fetching-includes) Note: If specifying the `include` option, all "default" includes will be ignored, and only the specified relationships included, per spec. #### Fields The `fields` option satisfies the [sparse fieldset](http://jsonapi.org/format/#fetching-sparse-fieldsets) portion of the spec. This options should be a map of resource types whose value is a comma separated list of fields to include. Example: `fields: %{"articles" => "title,body", "comments" => "body"}` If you're using Plug, you should be able to call `fetch_query_params(conn)` and pass the result of `conn.query_params["fields"]` as this option. ## Phoenix Usage For an example of starting with Phoenix's JSON generator and updating to work with JaSerializer, see [Getting Started with Phoenix](https://github.com/vt-elixir/ja_serializer/wiki/Getting-Started-with-Phoenix). Simply `use JaSerializer.PhoenixView` in your view (or in the Web module) and define your serializer as above. The `render("index.json-api", data)` and `render("show.json-api", data)` are defined for you. You can just call render as normal from your controller. By specifying `include`s when calling the render function, you can override the `include: false` in the ArticleView. ```elixir defmodule PhoenixExample.ArticlesController do use PhoenixExample.Web, :controller def index(conn, _params) do render conn, "index.json-api", data: Repo.all(Article) end def show(conn, %{"id" => id}) do article = Repo.get(Article, id) |> Repo.preload([:comments]) render conn, "show.json-api", data: article, opts: [include: "comments"] end def create(conn, %{"data" => data}) do attrs = JaSerializer.Params.to_attributes(data) changeset = Article.changeset(%Article{}, attrs) case Repo.insert(changeset) do {:ok, article} -> conn |> put_status(201) |> render("show.json-api", data: article) {:error, changeset} -> conn |> put_status(422) |> render(:errors, data: changeset) end end end defmodule PhoenixExample.ArticlesView do use PhoenixExample.Web, :view use JaSerializer.PhoenixView # Or use in web/web.ex attributes [:title] has_many :comments, serializer: PhoenixExample.CommentsView, include: false, identifiers: :when_included #has_many, etc. end ``` ## Configuration To use the Phoenix `accepts` plug you must configure Plug to handle the "application/vnd.api+json" mime type and Phoenix to serialize json-api with Poison. Depending on your version of Plug add the following to `config.exs`: Plug ~> "1.2.0" ```elixir config :phoenix, :format_encoders, "json-api": Poison config :mime, :types, %{ "application/vnd.api+json" => ["json-api"] } ``` And then re-compile mime: (per: https://hexdocs.pm/mime/MIME.html) ```shell mix deps.clean mime --build mix deps.get ``` Plug < "1.2.0" ```elixir config :phoenix, :format_encoders, "json-api": Poison config :plug, :mimes, %{ "application/vnd.api+json" => ["json-api"] } ``` And then re-compile plug: (per: https://hexdocs.pm/plug/1.1.3/Plug.MIME.html) ```shell mix deps.clean plug --build mix deps.get ``` And then add json api to your plug pipeline. ```elixir pipeline :api do plug :accepts, ["json-api"] end ``` For strict content-type/accept enforcement and to auto add the proper content-type to responses add the JaSerializer.ContentTypeNegotiation plug. To normalize attributes to underscores include the JaSerializer.Deserializer plug. ```elixir pipeline :api do plug :accepts, ["json-api"] plug JaSerializer.ContentTypeNegotiation plug JaSerializer.Deserializer end ``` If you're rendering JSON API errors, like `404.json-api`, then you _must_ add `json-api` to the `accepts` of your `render_errors` within your existing configuration in `config.exs`, like so: ```elixir config :phoenix, PhoenixExample.Endpoint, render_errors: [view: PhoenixExample.ErrorView, accepts: ~w(html json json-api)] ``` If you're rendering both JSON-API and HTML, you need to include the `html` option in the config: ```elixir config :phoenix, :format_encoders, html: Phoenix.Template.HTML, "json-api": Poison ``` ## Testing controllers Set the right headers in `setup` and when passing parameters to put and post requests, you should pass them as a binary. That is because for map and list parameters, the content-type will be automatically changed to multipart. ```elixir defmodule Sample.SomeControllerTest do use Sample.ConnCase setup %{conn: conn} do conn = conn |> put_req_header("accept", "application/vnd.api+json") |> put_req_header("content-type", "application/vnd.api+json") {:ok, conn: conn} end test "create action", %{conn: conn} do params = Poison.encode!(%{data: %{attributes: @valid_attrs}}) conn = post conn, "/some_resource", params ... end ... end ``` ## Pagination JaSerializer provides page based pagination integration with [Scrivener](https://github.com/drewolson/scrivener) or custom pagination by passing your owns links in. ### Custom JaSerializer allows custom pagination via the `page` option. The `page` option expects to receive a `Map` with URL values for `first`, `next`, `prev`, and `last`. For example: ```elixir page = %{ first: "http://example.com/api/v1/posts?page[cursor]=1&page[per]=20", prev: nil next: "http://example.com/api/v1/posts?page[cursor]=20&page[per]=20", last: "http://example.com/api/v1/posts?page[cursor]=60&page[per]=20" } # Direct call JaSerializer.format(MySerializer, collection, conn, page: page) # In Phoenix Controller render conn, "index.json-api", data: collection, opts: [page: page] ``` #### Builder You can build the pagination links with `JaSerializer.Builder.PaginationLinks.build/2` Simply pass in the following: ```elixir links = JaSerializer.Builder.PaginationLinks.build( %{ number: 2, size: 10, total: 20 }, conn ) ``` See `JaSerializer.Builder.PaginationLinks` for how to customize. ### Scrivener Integration If you are using Scrivener for pagination, all you need to do is pass the results of `paginate/2` to your serializer. ```elixir page = MyRepo.paginate(MyModel, params.page) # Direct call JaSerializer.format(MySerializer, page, conn, []) # In Phoenix controller render conn, "index.json-api", data: page ``` When integrating with Scrivener, the URLs generated will be based on the `Plug.Conn`'s path. This can be overridden by passing in the `page[:base_url]` option. ```elixir render conn, "index.json-api", data: page, opts: [base_url: "http://example.com/foos"] ``` You can also configure `ja_serializer` to use a global default URL base for all links. ```elixir config :ja_serializer, page_base_url: "http://example.com:4000/v1/" ``` *Note*: The resulting URLs will use the JSON-API recommended `page` query param. Example URL: `http://example.com:4000/v1/posts?page[page]=2&page[page-size]=50` ### Meta Data JaSerializer allows adding top level meta information via the `meta` option. The `meta` option expects to receive a `Map` containing the data which will be rendered under the top level meta key. ```elixir meta_data = %{ "key" => "value" } # Direct call JaSerializer.format(MySerializer, data, conn, meta: meta_data) # In Phoenix controller render conn, "index.json-api", data: data, opts: [meta: meta_data] ``` ## Customization ### Key Format (for Attribute, Relationship and Query Param) By default keys are `dash-erized` as per the JSON:API 1.0 recommendation, but keys can be customized via config. In your `config.exs` file you can use `camel_cased` recommended by upcoming JSON:API 1.1: ```elixir config :ja_serializer, key_format: :camel_cased ``` Or `underscored`: ```elixir config :ja_serializer, key_format: :underscored ``` You may also pass custom function for serialization and a second optional one for deserialization. Both accept a single binary argument: ```elixir defmodule MyStringModule do def camelize(key), do: key #... def underscore(key), do: key #... end config :ja_serializer, key_format: {:custom, MyStringModule, :camelize, :underscore} ``` ### Custom Attribute Value Formatters When serializing attribute values more complex than string, numbers, atoms or list of those things it is recommended to implement a custom formatter. To implement a custom formatter: ```elixir defimpl JaSerializer.Formatter, for: [MyStruct] do def format(struct), do: struct end ``` ### Pluralizing All Types By Default You can opt-in to pluralizing all types for default: ```elixir config :ja_serializer, pluralize_types: true ``` ## Complimentary Libraries * [JaResource](https://github.com/vt-elixir/ja_resource) - WIP behaviour for creating JSON-API controllers in Phoenix. * [voorhees](https://github.com/danmcclain/voorhees) - Testing tool for JSON API responses * [inquisitor](https://github.com/DockYard/inquisitor) - Composable query builder for Ecto * [scrivener](https://github.com/drewolson/scrivener) - Ecto pagination ## License JaSerializer source code is released under Apache 2 License. Check LICENSE file for more information.