Repository: jeregrine/jsonapi
Pull request #97 (2018-02-25 19:19:01)

include meta as top level document member
This PR adds the ability to pass a `meta` map to the render function.

The current functionality relies on the meta data to be present on the `data` attribute.  However, for example, on index routes, `data` is simply a List and some attributes to filter/paginate the list may need to be passed through to the response that aren't implicitly included/apart of the `data`.

Lmk your thoughts!

Ref #96 

snewcomer: Another thought is ensure a user defines a key to put on the `conn` struct and if present, attach to the payload instead of adding another argument to serialize.  
snewcomer: @jeregrine Looks like 18.3 is failing here and on master.  Only on elixir 1.6 is otp 18 not supported.  Would you like me to remove `18.3` from travis?  Or is there another way to do this?

https://github.com/elixir-lang/elixir/blob/master/lib/elixir/pages/Compatibility%20and%20Deprecations.md#compatibility-between-elixir-and-erlangotp

https://travis-ci.org/jeregrine/jsonapi/jobs/347070137
https://travis-ci.org/jeregrine/jsonapi/jobs/346951834
jeregrine: Yea we just need to update the travis matrix a good example is
https://github.com/getsentry/sentry-elixir/blob/master/.travis.yml

On Mon, Mar 5, 2018 at 11:46 AM, Scott Newcomer <notifications@github.com>
wrote:

> @jeregrine <https://github.com/jeregrine> Looks like 18.3 is failing here
> and on master. Only on elixir 1.6 is otp 18 not supported. Would you like
> me to remove 18.3 from travis? Or is there another way to do this?
>
> https://github.com/elixir-lang/elixir/blob/master/lib/
> elixir/pages/Compatibility%20and%20Deprecations.md#
> compatibility-between-elixir-and-erlangotp
>
> https://travis-ci.org/jeregrine/jsonapi/jobs/347070137
> https://travis-ci.org/jeregrine/jsonapi/jobs/346951834
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/jeregrine/jsonapi/pull/97#issuecomment-370502904>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAGKFnjK52P43bFQdQlCA1hCFXnYdKwGks5tbXoKgaJpZM4SSa0_>
> .
>

doomspork: @snewcomer could you squash your commits to re-trigger Travis? 😁 

Patch:
diff --git a/README.md b/README.md
index a60a9b3..5dc3e3c 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ A project that will render your data models into [JSONAPI Documents](http://json
 - [X] Handling of sparse fieldsets
 - [X] Handling of includes
 - [X] Handling of pagination
+- [X] Handling of top level meta data
 
 ## How to use with Phoenix
 
@@ -33,6 +34,12 @@ defmodule MyApp.PostView do
     String.slice(post.body, 0..5)
   end
 
+  def meta(data, _conn) do
+    # this will add meta to each record
+    # To add meta as a top level property, pass as argument to render function (shown below)
+    %{meta_text: "meta_#{data[:text]}"}
+  end
+
   def relationships do
     # The post's author will be included by default
     [author: {MyApp.UserView, :include},
@@ -40,9 +47,9 @@ defmodule MyApp.PostView do
   end
 end
 ```
-is an example of a basic view. You can now call `render(conn, MyApp.PostView, "show.json", %{data: my_data})` or `'index.json` normally.
+is an example of a basic view. You can now call `render(conn, MyApp.PostView, "show.json", %{data: my_data, meta: meta})` or `'index.json` normally.
 
-If you'd like to use this without phoenix simply use the `JSONAPI.View` and call `JSONAPI.Serializer.serialize(MyApp.PostView, data, conn)`.
+If you'd like to use this without phoenix simply use the `JSONAPI.View` and call `JSONAPI.Serializer.serialize(MyApp.PostView, data, conn, meta)`.
 
 ## Parsing and validating a JSONAPI Request
 
diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex
index fafadd1..e0fd14e 100644
--- a/lib/jsonapi/serializer.ex
+++ b/lib/jsonapi/serializer.ex
@@ -13,7 +13,7 @@ defmodule JSONAPI.Serializer do
   Please refer to `JSONAPI.View` for more information. If you are in interested in relationships
   and includes you may also want to reference the `JSONAPI.QueryParser`.
   """
-  def serialize(view, data, conn \\ nil) do
+  def serialize(view, data, conn \\ nil, meta \\ nil) do
     query_includes =
       case conn do
         %Plug.Conn{assigns: %{jsonapi_query: %{includes: includes}}} -> includes
@@ -24,7 +24,8 @@ defmodule JSONAPI.Serializer do
 
     encoded_data = %{
       data: encoded_data,
-      included: flatten_included(to_include)
+      included: flatten_included(to_include),
+      meta: meta
     }
 
     merge_links(encoded_data, data, view, conn, remove_links?())
diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex
index 7e1ab0f..7de775f 100644
--- a/lib/jsonapi/view.ex
+++ b/lib/jsonapi/view.ex
@@ -62,7 +62,7 @@ defmodule JSONAPI.View do
 
   ## Options
     * `:host` (binary) - Allows the `host` to be overrided for generated URLs.  Defaults to `host` of the supplied `conn`.
-    
+
     * `:scheme` (atom) - Enables configuration of the HTTP scheme for generated URLS.  Defaults to `scheme` from the provided `conn`.
 
     * `:underscore_to_dash` (boolean) - Use dash (`-`) as the word separated for JSON in place of underscore (`_`) per the JSONAPI spec [recommendations](http://jsonapi.org/recommendations/).  Defaults to `false`.
@@ -75,7 +75,7 @@ defmodule JSONAPI.View do
     {namespace, _opts} = Keyword.pop(opts, :namespace, "")
 
     quote do
-      import JSONAPI.Serializer, only: [serialize: 3]
+      import JSONAPI.Serializer, only: [serialize: 4]
 
       @resource_type unquote(type)
       @namespace unquote(namespace)
@@ -113,8 +113,8 @@ defmodule JSONAPI.View do
 
       def hidden, do: []
 
-      def show(model, conn, _params), do: serialize(__MODULE__, model, conn)
-      def index(models, conn, _params), do: serialize(__MODULE__, models, conn)
+      def show(model, conn, _params, meta \\ nil), do: serialize(__MODULE__, model, conn, meta)
+      def index(models, conn, _params, meta \\ nil), do: serialize(__MODULE__, models, conn, meta)
 
       def url_for(nil, nil) do
         "#{@namespace}/#{type()}"
@@ -141,15 +141,21 @@ defmodule JSONAPI.View do
       end
 
       if Code.ensure_loaded?(Phoenix) do
+        def render("show.json", %{data: data, conn: conn, params: params, meta: meta}),
+          do: show(data, conn, params, meta: meta)
+
+        def render("show.json", %{data: data, conn: conn, meta: meta}),
+          do: show(data, conn, conn.params, meta: meta)
+
         def render("show.json", %{data: data, conn: conn}), do: show(data, conn, conn.params)
 
-        def render("show.json", %{data: data, conn: conn, params: params}),
-          do: show(data, conn, params)
+        def render("index.json", %{data: data, conn: conn, params: params, meta: meta}),
+          do: index(data, conn, params, meta)
 
-        def render("index.json", %{data: data, conn: conn}), do: index(data, conn, conn.params)
+        def render("index.json", %{data: data, conn: conn, meta: meta}),
+          do: index(data, conn, conn.params, meta)
 
-        def render("index.json", %{data: data, conn: conn, params: params}),
-          do: index(data, conn, params)
+        def render("index.json", %{data: data, conn: conn}), do: index(data, conn, conn.params)
       end
 
       defp host(conn), do: Application.get_env(:jsonapi, :host, conn.host)


Test patch:
diff --git a/test/jsonapi_test.exs b/test/jsonapi_test.exs
index f9f6062..5c129aa 100644
--- a/test/jsonapi_test.exs
+++ b/test/jsonapi_test.exs
@@ -72,7 +72,7 @@ defmodule JSONAPITest do
     defp passthrough(conn, _) do
       resp =
         PostView
-        |> JSONAPI.Serializer.serialize(conn.assigns[:data], conn)
+        |> JSONAPI.Serializer.serialize(conn.assigns[:data], conn, conn.assigns[:meta])
         |> Poison.encode!()
 
       Plug.Conn.send_resp(conn, 200, resp)
@@ -92,12 +92,15 @@ defmodule JSONAPITest do
           other_user: %{username: "josh", id: 3}
         }
       ])
+      |> Plug.Conn.assign(:meta, %{total_pages: 1})
       |> MyPostPlug.call([])
 
     json = conn.resp_body |> Poison.decode!()
 
     assert Map.has_key?(json, "data")
     data_list = Map.get(json, "data")
+    meta = Map.get(json, "meta")
+    assert meta["total_pages"] == 1
 
     assert Enum.count(data_list) == 1
     [data | _] = data_list
diff --git a/test/serializer_test.exs b/test/serializer_test.exs
index 23b813b..727f374 100644
--- a/test/serializer_test.exs
+++ b/test/serializer_test.exs
@@ -80,6 +80,15 @@ defmodule JSONAPISerializerTest do
     end
   end
 
+  test "serialize includes meta as top level member" do
+    meta = %{total_pages: 10}
+    encoded = Serializer.serialize(PostView, %{id: 1, text: "Hello"}, nil, meta)
+    assert %{total_pages: 10} = encoded[:meta]
+
+    encoded = Serializer.serialize(CommentView, %{id: 1}, nil, nil)
+    assert encoded[:meta] == nil
+  end
+
   test "serialize only includes meta if provided" do
     encoded = Serializer.serialize(PostView, %{id: 1, text: "Hello"}, nil)
     assert %{meta_text: "meta_Hello"} = encoded[:data][:meta]