Repository: jeregrine/jsonapi
Pull request #178 (2019-02-11 00:57:21)

Globally Configurable Namespace
Why:

* A lot of the times a namespace is defined is to add the `/api` prefix
  to all the routes, and having to add it to all the views becomes tedious
  and error prone

This change addresses the need by:

* Allowing the namespace to be defined in a global config as a default
  that can be changed on a module by module basis

Note
-----

This pull request supersedes #130 by @zamith . Big ups to @zamith for the original effort 💪 .


Patch:
diff --git a/README.md b/README.md
index 963397d..0471bf8 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,7 @@ Under-the-hood `JSONAPI.EnsureSpec` relies on three individual plugs:
 config :jsonapi,
   host: "www.someotherhost.com",
   scheme: "https",
+  namespace: "/api",
   field_transformation: :underscore,
   remove_links: false,
   json_library: Jason
@@ -140,6 +141,10 @@ config :jsonapi,
 
 - **host**, **scheme**. By default these are pulled from the `conn`, but may be
   overridden.
+- **namespace**. This optional setting can be used to configure the namespace
+  your resources live at (e.g. given "http://example.com/api/cars", `"/api"`
+  would be the namespace). See also `JSONAPI.View` for setting on the resource's
+  View itself.
 - **remove_links**. `links` data can optionally be removed from the payload via
   setting the configuration above to `true`. Defaults to `false`.
 - **json_library**. Defaults to [Jason](https://hex.pm/packages/jason).
diff --git a/lib/jsonapi/view.ex b/lib/jsonapi/view.ex
index af6e5fc..ec2dbbc 100644
--- a/lib/jsonapi/view.ex
+++ b/lib/jsonapi/view.ex
@@ -32,6 +32,10 @@ defmodule JSONAPI.View do
         end
       end
 
+      defmodule DogView do
+        use JSONAPI.View, namespace: "/pupperz-api"
+      end
+
   You can now call `UserView.show(user, conn, conn.params)` and it will render
   a valid jsonapi doc.
 
@@ -77,16 +81,21 @@ defmodule JSONAPI.View do
   the serializer to *always* include if its loaded.
 
   ## Options
-    * `:host` (binary) - Allows the `host` to be overrided for generated URLs.  Defaults to `host` of the supplied `conn`.
+    * `: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`.
 
+    * `:namespace` (binary) - Allows the namespace of a given resource. This may be
+      configured globally or overridden on the View itself. Note that if you have
+      a globally defined namespace and need to *remove* the namespace for a
+      resource, set the namespace to a blank String.
+
   The default behaviour for `host` and `scheme` is to derive it from the `conn` provided, while the
   default style for presentation in names is to be underscored and not dashed.
   """
   defmacro __using__(opts \\ []) do
     {type, opts} = Keyword.pop(opts, :type)
-    {namespace, _opts} = Keyword.pop(opts, :namespace, "")
+    {namespace, _opts} = Keyword.pop(opts, :namespace)
 
     quote do
       import JSONAPI.Serializer, only: [serialize: 4]
@@ -104,6 +113,12 @@ defmodule JSONAPI.View do
         def type, do: raise("Need to implement type/0")
       end
 
+      if @namespace do
+        def namespace, do: @namespace
+      else
+        def namespace, do: Application.get_env(:jsonapi, :namespace, "")
+      end
+
       def attributes(data, conn) do
         hidden = hidden(data)
 
@@ -134,23 +149,23 @@ defmodule JSONAPI.View do
       def index(models, conn, _params, meta \\ nil), do: serialize(__MODULE__, models, conn, meta)
 
       def url_for(nil, nil) do
-        "#{@namespace}/#{type()}"
+        "#{namespace()}/#{type()}"
       end
 
       def url_for(data, nil) when is_list(data) do
-        "#{@namespace}/#{type()}"
+        "#{namespace()}/#{type()}"
       end
 
       def url_for(data, nil) do
-        "#{@namespace}/#{type()}/#{id(data)}"
+        "#{namespace()}/#{type()}/#{id(data)}"
       end
 
       def url_for(data, %Plug.Conn{} = conn) when is_list(data) do
-        "#{scheme(conn)}://#{host(conn)}#{@namespace}/#{type()}"
+        "#{scheme(conn)}://#{host(conn)}#{namespace()}/#{type()}"
       end
 
       def url_for(data, %Plug.Conn{} = conn) do
-        "#{scheme(conn)}://#{host(conn)}#{@namespace}/#{type()}/#{id(data)}"
+        "#{scheme(conn)}://#{host(conn)}#{namespace()}/#{type()}/#{id(data)}"
       end
 
       def url_for_rel(data, rel_type, conn) do


Test patch:
diff --git a/test/jsonapi/view_test.exs b/test/jsonapi/view_test.exs
index 87fa90f..d395c41 100644
--- a/test/jsonapi/view_test.exs
+++ b/test/jsonapi/view_test.exs
@@ -24,9 +24,7 @@ defmodule JSONAPI.ViewTest do
   end
 
   defmodule UserView do
-    use JSONAPI.View,
-      type: "users",
-      namespace: "/api"
+    use JSONAPI.View, type: "users"
 
     def fields do
       [:age, :first_name, :last_name, :full_name, :password]
@@ -41,13 +39,17 @@ defmodule JSONAPI.ViewTest do
     end
   end
 
-  alias JSONAPI.ViewTest.CommentView
+  defmodule CarView do
+    use JSONAPI.View, type: "cars", namespace: ""
+  end
 
   setup do
     Application.put_env(:jsonapi, :field_transformation, :underscore)
+    Application.put_env(:jsonapi, :namespace, "/other-api")
 
     on_exit(fn ->
       Application.delete_env(:jsonapi, :field_transformation)
+      Application.delete_env(:jsonapi, :namespace)
     end)
 
     {:ok, []}
@@ -57,38 +59,90 @@ defmodule JSONAPI.ViewTest do
     assert PostView.type() == "posts"
   end
 
-  test "url_for/2" do
-    assert PostView.url_for(nil, nil) == "/api/posts"
-    assert PostView.url_for([], nil) == "/api/posts"
-    assert PostView.url_for(%{id: 1}, nil) == "/api/posts/1"
-    assert PostView.url_for([], %Plug.Conn{}) == "http://www.example.com/api/posts"
-    assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "http://www.example.com/api/posts/1"
+  describe "namespace/0" do
+    setup do
+      Application.put_env(:jsonapi, :namespace, "/cake")
 
-    assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
-             "http://www.example.com/api/posts/relationships/comments"
+      on_exit(fn ->
+        Application.delete_env(:jsonapi, :namespace)
+      end)
 
-    assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
-             "http://www.example.com/api/posts/1/relationships/comments"
+      {:ok, []}
+    end
 
-    Application.put_env(:jsonapi, :host, "www.otherhost.com")
-    assert PostView.url_for([], %Plug.Conn{}) == "http://www.otherhost.com/api/posts"
-    assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "http://www.otherhost.com/api/posts/1"
+    test "uses macro configuration first" do
+      assert PostView.namespace() == "/api"
+    end
 
-    assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
-             "http://www.otherhost.com/api/posts/relationships/comments"
+    test "uses global namespace if available" do
+      assert UserView.namespace() == "/cake"
+    end
+
+    test "can be blank" do
+      assert CarView.namespace() == ""
+    end
+  end
 
-    assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
-             "http://www.otherhost.com/api/posts/1/relationships/comments"
+  describe "url_for/2 when host and scheme not configured" do
+    test "url_for/2" do
+      assert PostView.url_for(nil, nil) == "/api/posts"
+      assert PostView.url_for([], nil) == "/api/posts"
+      assert PostView.url_for(%{id: 1}, nil) == "/api/posts/1"
+      assert PostView.url_for([], %Plug.Conn{}) == "http://www.example.com/api/posts"
+      assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "http://www.example.com/api/posts/1"
 
-    Application.put_env(:jsonapi, :scheme, "ftp")
-    assert PostView.url_for([], %Plug.Conn{}) == "ftp://www.otherhost.com/api/posts"
-    assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "ftp://www.otherhost.com/api/posts/1"
+      assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
+               "http://www.example.com/api/posts/relationships/comments"
 
-    assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
-             "ftp://www.otherhost.com/api/posts/relationships/comments"
+      assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
+               "http://www.example.com/api/posts/1/relationships/comments"
+    end
+  end
+
+  describe "url_for/2 when host configured" do
+    setup do
+      Application.put_env(:jsonapi, :host, "www.otherhost.com")
+
+      on_exit(fn ->
+        Application.delete_env(:jsonapi, :host)
+      end)
+
+      {:ok, []}
+    end
 
-    assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
-             "ftp://www.otherhost.com/api/posts/1/relationships/comments"
+    test "uses configured host instead of that on Conn" do
+      assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
+               "http://www.otherhost.com/api/posts/relationships/comments"
+
+      assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
+               "http://www.otherhost.com/api/posts/1/relationships/comments"
+
+      assert PostView.url_for([], %Plug.Conn{}) == "http://www.otherhost.com/api/posts"
+      assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "http://www.otherhost.com/api/posts/1"
+    end
+  end
+
+  describe "url_for/2 when scheme configured" do
+    setup do
+      Application.put_env(:jsonapi, :scheme, "ftp")
+
+      on_exit(fn ->
+        Application.delete_env(:jsonapi, :scheme)
+      end)
+
+      {:ok, []}
+    end
+
+    test "uses configured scheme instead of that on Conn" do
+      assert PostView.url_for([], %Plug.Conn{}) == "ftp://www.example.com/api/posts"
+      assert PostView.url_for(%{id: 1}, %Plug.Conn{}) == "ftp://www.example.com/api/posts/1"
+
+      assert PostView.url_for_rel([], "comments", %Plug.Conn{}) ==
+               "ftp://www.example.com/api/posts/relationships/comments"
+
+      assert PostView.url_for_rel(%{id: 1}, "comments", %Plug.Conn{}) ==
+               "ftp://www.example.com/api/posts/1/relationships/comments"
+    end
   end
 
   test "url_for_pagination/3" do