Repository: jeregrine/jsonapi
Pull request #214 (2019-10-23 10:45:25)

Add query parameters to the links
Query parameters are missing from the generated payload and pagination links which only include pagination information (page offset and size).

doomspork: We also need to make sure the build passes @komlanvi 👍 
komlanvi: Hi @doomspork @jherdman any news about this?
komlanvi: Any comment ? @doomspork @jeregrine @mitchellhenke @snewcomer 
doomspork: Things look good to me :grin: 

Patch:
diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex
index 0f1d973..39a8ae8 100644
--- a/lib/jsonapi/serializer.ex
+++ b/lib/jsonapi/serializer.ex
@@ -47,6 +47,8 @@ defmodule JSONAPI.Serializer do
     merge_links(encoded_data, data, view, conn, query_page, remove_links?(), options)
   end
 
+  def encode_data(_view, nil, _conn, _query_includes, _options), do: {[], nil}
+
   def encode_data(view, data, conn, query_includes, options) when is_list(data) do
     Enum.map_reduce(data, [], fn d, acc ->
       {to_include, encoded_data} = encode_data(view, d, conn, query_includes, options)
diff --git a/lib/jsonapi/utils/list.ex b/lib/jsonapi/utils/list.ex
new file mode 100644
index 0000000..c0a9d88
--- /dev/null
+++ b/lib/jsonapi/utils/list.ex
@@ -0,0 +1,44 @@
+defmodule JSONAPI.Utils.List do
+  @moduledoc false
+
+  @doc """
+  Transforms a Map into a List of Tuples that can be converted into a query string via URI.encode_query/1
+
+  ## Examples
+
+      iex> to_list_of_query_string_components(%{"number" => 5})
+      [{"number", 5}]
+
+      iex> to_list_of_query_string_components(%{color: "red"})
+      [{:color, "red"}]
+
+      iex> to_list_of_query_string_components(%{"alphabet" => ["a", "b", "c"]})
+      [{"alphabet[]", "a"}, {"alphabet[]", "b"}, {"alphabet[]", "c"}]
+
+      iex> to_list_of_query_string_components(%{"filters" => %{"age" => 18, "name" => "John"}})
+      [{"filters[age]", 18}, {"filters[name]", "John"}]
+
+  """
+  @spec to_list_of_query_string_components(map()) :: list(tuple())
+  def to_list_of_query_string_components(map) when is_map(map) do
+    Enum.flat_map(map, &do_to_list_of_query_string_components/1)
+  end
+
+  defp do_to_list_of_query_string_components({key, value}) when is_list(value) do
+    to_list_of_two_elem_tuple(key, value)
+  end
+
+  defp do_to_list_of_query_string_components({key, value}) when is_map(value) do
+    Enum.flat_map(value, fn {k, v} -> to_list_of_two_elem_tuple("#{key}[#{k}]", v) end)
+  end
+
+  defp do_to_list_of_query_string_components({key, value}), do: to_list_of_two_elem_tuple(key, value)
+
+  defp to_list_of_two_elem_tuple(key, value) when is_list(value) do
+    Enum.map(value, &{"#{key}[]", &1})
+  end
+
+  defp to_list_of_two_elem_tuple(key, value) do
+    [{key, value}]
+  end
+end
diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex
index f9258c6..88dc54d 100644
--- a/lib/jsonapi/view.ex
+++ b/lib/jsonapi/view.ex
@@ -120,6 +120,7 @@ defmodule JSONAPI.View do
     {paginator, _opts} = Keyword.pop(opts, :paginator)
 
     quote do
+      import JSONAPI.Utils.List, only: [to_list_of_query_string_components: 1]
       import JSONAPI.Serializer, only: [serialize: 5]
 
       @resource_type unquote(type)
@@ -217,7 +218,7 @@ defmodule JSONAPI.View do
 
       def url_for(data, nil), do: "#{namespace()}/#{type()}/#{id(data)}"
 
-      def url_for(data, %Plug.Conn{} = conn) when is_list(data) do
+      def url_for(data, %Plug.Conn{} = conn) when is_list(data) or is_nil(data) do
         "#{scheme(conn)}://#{host(conn)}#{namespace()}/#{type()}"
       end
 
@@ -229,11 +230,10 @@ defmodule JSONAPI.View do
         "#{url_for(data, conn)}/relationships/#{rel_type}"
       end
 
-      def url_for_pagination(data, conn, pagination_attrs) do
-        pagination_attrs
-        |> Enum.reduce(%{}, fn {key, value}, acc ->
-          Map.put(acc, "page[#{key}]", value)
-        end)
+      def url_for_pagination(data, %{query_params: query_params} = conn, pagination_attrs) do
+        query_params
+        |> Map.put("page", pagination_attrs)
+        |> to_list_of_query_string_components()
         |> URI.encode_query()
         |> prepare_url(data, conn)
       end


Test patch:
diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs
index 7a9b460..d246360 100644
--- a/test/jsonapi/serializer_test.exs
+++ b/test/jsonapi/serializer_test.exs
@@ -142,6 +142,8 @@ defmodule JSONAPI.SerializerTest do
 
     def type, do: "expensive-resource"
 
+    def links(nil, _conn), do: %{}
+
     def links(data, _conn) do
       %{
         queue: "/expensive-resource/queue/#{data.id}",
@@ -590,6 +592,7 @@ defmodule JSONAPI.SerializerTest do
       :get
       |> Plug.Test.conn("/mytype?page[page]=2&page[size]=1")
       |> QueryParser.call(%Config{view: view, opts: []})
+      |> Plug.Conn.fetch_query_params()
 
     encoded =
       Serializer.serialize(PaginatedPostView, data, conn, nil, total_pages: 3, total_items: 3)
@@ -636,4 +639,14 @@ defmodule JSONAPI.SerializerTest do
 
     assert expected_links == links
   end
+
+  test "serialize returns a null data if it receives a null data" do
+    assert %{
+             data: data,
+             links: links
+           } = Serializer.serialize(ExpensiveResourceView, nil, nil)
+
+    assert nil == data
+    assert %{self: "/expensive-resource"} == links
+  end
 end
diff --git a/test/jsonapi/view_test.exs b/test/jsonapi/view_test.exs
index 932de05..ba2f922 100644
--- a/test/jsonapi/view_test.exs
+++ b/test/jsonapi/view_test.exs
@@ -145,11 +145,28 @@ defmodule JSONAPI.ViewTest do
     end
   end
 
-  test "url_for_pagination/3" do
-    assert PostView.url_for_pagination(nil, nil, %{}) == "/api/posts"
+  describe "url_for_pagination/3" do
+    setup do
+      {:ok, conn: Plug.Conn.fetch_query_params(%Plug.Conn{})}
+    end
+
+    test "with pagination information", %{conn: conn} do
+      assert PostView.url_for_pagination(nil, conn, %{}) == "http://www.example.com/api/posts"
+
+      assert PostView.url_for_pagination(nil, conn, %{number: 1, size: 10}) ==
+               "http://www.example.com/api/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10"
+    end
+
+    test "with query parameters", %{conn: conn} do
+      conn_with_query_params =
+        Kernel.update_in(conn.query_params, &Map.put(&1, "comments", [5, 2]))
 
-    assert PostView.url_for_pagination(nil, nil, %{number: 1, size: 10}) ==
-             "/api/posts?page%5Bnumber%5D=1&page%5Bsize%5D=10"
+      assert PostView.url_for_pagination(nil, conn_with_query_params, %{number: 1, size: 10}) ==
+               "http://www.example.com/api/posts?comments%5B%5D=5&comments%5B%5D=2&page%5Bnumber%5D=1&page%5Bsize%5D=10"
+
+      assert PostView.url_for_pagination(nil, conn_with_query_params, %{}) ==
+               "http://www.example.com/api/posts?comments%5B%5D=5&comments%5B%5D=2"
+    end
   end
 
   test "render/2 is defined when 'Phoenix' is loaded" do
diff --git a/test/utils/list_test.exs b/test/utils/list_test.exs
new file mode 100644
index 0000000..b71611a
--- /dev/null
+++ b/test/utils/list_test.exs
@@ -0,0 +1,9 @@
+defmodule JSONAPI.Utils.ListTest do
+  @moduledoc false
+
+  use ExUnit.Case, async: true
+
+  import JSONAPI.Utils.List
+
+  doctest JSONAPI.Utils.List
+end