Repository: jeregrine/jsonapi
Pull request #77 (2017-10-09 20:40:08)

Dashes in relationships key
Not small refactor of `underscore_to_dash` to make relationship keys also have that functionality.

if `underscore_to_dash` is set, relationship keys will also now have their underscores replaced by dashes.

```elixir
%{
  data: %{
    attributes: %{
      full_description: "description"
    }
  },
  relationships: %{
    best_comments: []
  }
}
```

We'll get:

```elixir
%{
  data: %{
    attributes: %{
      "full-description": "description"
    }
  },
  relationships: %{
    "best-comments": []
  }
}
```

doomspork: @mitchellhenke looks like we're missing some things Credo requires.  Would you mind squashing your commits? 😁 
mitchellhenke: @doomspork I've updated per your comments with the exception of the async tests, as I'm not sure how to best work around that.

Patch:
diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex
index 32846f0..aea35ab 100644
--- a/lib/jsonapi/serializer.ex
+++ b/lib/jsonapi/serializer.ex
@@ -4,6 +4,7 @@ defmodule JSONAPI.Serializer do
   """
 
   import JSONAPI.Ecto, only: [assoc_loaded?: 1]
+  alias JSONAPI.Utils.Underscore
 
   @doc """
   Takes a view, data and a optional plug connection and returns a fully JSONAPI Serialized document.
@@ -41,7 +42,7 @@ defmodule JSONAPI.Serializer do
     doc = %{
       id: view.id(data),
       type: view.type(),
-      attributes: view.attributes(data, conn),
+      attributes: underscore(view.attributes(data, conn)),
       relationships: %{},
       links: %{
         self: view.url_for(data, conn)
@@ -75,7 +76,7 @@ defmodule JSONAPI.Serializer do
     # Build the relationship url
     rel_url = view.url_for_rel(data, key, conn)
     # Build the relationship
-    acc = put_in(acc, [:relationships, key], encode_relation(only_rel_view, rel_data, rel_url, conn))
+    acc = put_in(acc, [:relationships, underscore(key)], encode_relation(only_rel_view, rel_data, rel_url, conn))
 
     valid_include_view = include_view(valid_includes, key)
 
@@ -163,4 +164,12 @@ defmodule JSONAPI.Serializer do
 
   def get_view({view, :include}), do: view
   def get_view(view), do: view
+
+  def underscore(data) do
+    if Underscore.underscore?() do
+      Underscore.underscore(data)
+    else
+      data
+    end
+  end
 end
diff --git a/lib/jsonapi/utils/underscore.ex b/lib/jsonapi/utils/underscore.ex
new file mode 100644
index 0000000..133eaa7
--- /dev/null
+++ b/lib/jsonapi/utils/underscore.ex
@@ -0,0 +1,31 @@
+defmodule JSONAPI.Utils.Underscore do
+  @moduledoc """
+  Helpers for replacing underscores with dashes.
+  """
+
+  def underscore?, do: Application.get_env(:jsonapi, :underscore_to_dash, false)
+
+  def underscore(value) when is_atom(value) do
+    value
+    |> to_string
+    |> underscore
+  end
+
+  def underscore(value) when is_binary(value) do
+    String.replace(value, "_", "-")
+  end
+
+  def underscore(value) when is_map(value) do
+    value
+    |> Enum.map(&underscore/1)
+    |> Enum.into(%{})
+  end
+
+  def underscore({key, value}) do
+    if is_map(value) do
+      {underscore(key), underscore(value)}
+    else
+      {underscore(key), value}
+    end
+  end
+end
diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex
index aae4470..b1ccea2 100644
--- a/lib/jsonapi/view.ex
+++ b/lib/jsonapi/view.ex
@@ -90,15 +90,8 @@ defmodule JSONAPI.View do
         def type, do: raise "Need to implement type/0"
       end
 
-      #TODO Figure out the nesting of fields
       def attributes(data, conn) do
-        data = visible_fields(data)
-
-        if underscore?() do
-          underscore(data)
-        else
-          data
-        end
+        visible_fields(data)
       end
 
       def meta(_data, _conn), do: nil
@@ -152,21 +145,6 @@ defmodule JSONAPI.View do
 
       defp visible_fields(data), do: Map.take(data, fields() -- hidden())
 
-      defp underscore?, do: Application.get_env(:jsonapi, :underscore_to_dash, false)
-
-      defp underscore(data) do
-        data
-        |> Enum.map(fn {key, value} -> {underscore_key(key), value} end)
-        |> Enum.into(%{})
-      end
-
-      defp underscore_key(key) do
-        key
-        |> to_string
-        |> String.replace("_", "-")
-        |> String.to_atom
-      end
-
       defp host(conn), do: Application.get_env(:jsonapi, :host, conn.host)
 
       defp scheme(conn), do: Application.get_env(:jsonapi, :scheme, to_string(conn.scheme))


Test patch:
diff --git a/test/jsonapi/view_test.exs b/test/jsonapi/view_test.exs
index cb1ad01..0fe58d4 100644
--- a/test/jsonapi/view_test.exs
+++ b/test/jsonapi/view_test.exs
@@ -72,11 +72,4 @@ defmodule JSONAPI.ViewTest do
     expected_map = %{age: 100, first_name: "Jason", last_name: "S"}
     assert expected_map == UserView.attributes(%{age: 100, first_name: "Jason", last_name: "S", password: "securepw"}, nil)
   end
-
-  test "attributes/2 with `:underscore_to_dash` option" do
-    Application.put_env(:jsonapi, :underscore_to_dash, true)
-    expected_map = %{age: 100, "first-name": "Jason", "last-name": "S"}
-    assert expected_map == UserView.attributes(%{age: 100, first_name: "Jason", last_name: "S"}, nil)
-    Application.put_env(:jsonapi, :underscore_to_dash, false)
-  end
 end
diff --git a/test/serializer_test.exs b/test/serializer_test.exs
index e7fee72..a9c6e4d 100644
--- a/test/serializer_test.exs
+++ b/test/serializer_test.exs
@@ -1,23 +1,24 @@
 defmodule JSONAPISerializerTest do
-  use ExUnit.Case
+  use ExUnit.Case, async: false
   alias JSONAPI.Serializer
 
   defmodule PostView do
     use JSONAPI.View
 
-    def fields, do: [:text, :body]
+    def fields, do: [:text, :body, :full_description]
     def meta(data, _conn), do: %{meta_text: "meta_#{data[:text]}"}
     def type, do: "mytype"
     def relationships do
       [author: {JSONAPISerializerTest.UserView, :include},
-       comments: {JSONAPISerializerTest.CommentView, :include}]
+       best_comments: {JSONAPISerializerTest.CommentView, :include}
+      ]
     end
   end
 
   defmodule UserView do
     use JSONAPI.View
 
-    def fields, do: [:username]
+    def fields, do: [:username, :first_name, :last_name]
     def type, do: "user"
     def relationships, do: []
   end
@@ -46,7 +47,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: [
+      best_comments: [
         %{ id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}},
         %{ id: 6, text: "not so great", user: %{id: 2, username: "jason"}}
       ]
@@ -75,7 +76,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: [
+      best_comments: [
         %{ id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}},
         %{ id: 6, text: "not so great", user: %{id: 2, username: "jason"}}
       ]
@@ -105,7 +106,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: []
+      best_comments: []
     }
 
     encoded = Serializer.serialize(PostView, data, nil)
@@ -130,7 +131,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: nil
+      best_comments: nil
     }
 
     encoded = Serializer.serialize(PostView, data, nil)
@@ -155,7 +156,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: []
+      best_comments: []
     }
 
     encoded = Serializer.serialize(PostView, data, nil)
@@ -170,7 +171,7 @@ defmodule JSONAPISerializerTest do
       text: "Hello",
       body: "Hello world",
       author: %{ id: 2, username: "jason"},
-      comments: [
+      best_comments: [
         %{id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}},
         %{id: 6, text: "not so great", user: %{id: 2, username: "jason"}}
       ]
@@ -179,7 +180,7 @@ defmodule JSONAPISerializerTest do
     conn = %Plug.Conn{
       assigns: %{
         jsonapi_query: %{
-          includes: [comments: :user]
+          includes: [best_comments: :user]
         }
       }
     }
@@ -189,4 +190,32 @@ defmodule JSONAPISerializerTest do
     assert encoded.data.relationships.author.links.self == "http://www.example.com/mytype/1/relationships/author"
     assert Enum.count(encoded.included) == 4
   end
+
+  test "serialize properly uses underscore_to_dash on both attributes and relationships" do
+    data = %{
+      id: 1,
+      text: "Hello",
+      body: "Hello world",
+      full_description: "This_is_my_description",
+      author: %{ id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"},
+      best_comments: [
+        %{id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack", last_name: "bronds"}},
+      ]
+    }
+
+    Application.put_env(:jsonapi, :underscore_to_dash, true)
+
+    encoded = Serializer.serialize(PostView, data, nil)
+
+    attributes = encoded[:data][:attributes]
+    relationships = encoded[:data][:relationships]
+    included = encoded[:included]
+
+    assert attributes["full-description"] == data[:full_description]
+    assert Enum.find(included, fn(i) -> i[:type] == "user" && i[:id] == "2" end)[:attributes]["last-name"] == "bonds"
+    assert Enum.find(included, fn(i) -> i[:type] == "user" && i[:id] == "4" end)[:attributes]["last-name"] == "bronds"
+    assert List.first(relationships["best-comments"][:data])[:id] == "5"
+
+    Application.delete_env(:jsonapi, :underscore_to_dash)
+  end
 end