Repository: phoenixframework/phoenix (elixir)

Issue:
Phoenix.View.render/3 raises when given a Layout struct
### Environment

* Elixir version (elixir -v): Elixir 1.6.6 (compiled with OTP 19)
* Phoenix version (mix deps): 1.3.2
* Operating system: Ubuntu 18.04

### Expected behavior

We have a `Layout` struct that we want to render via a json API.

```elixir
defmodule API.V1.LayoutView do
  use API, :view

  alias API.V1.LayoutView

  def render("index.json", %{layouts: results}) do
    %{
      layouts: render_many(results.data, LayoutView, "layout.json"),
      meta: results.meta
    }
  end

  def render("layout.json", %{layout: layout}) do
    %{
      id: layout.id,
      title: layout.title,
      tags: layout.tags,
      image_url: layout.image_url
    }
  end
end
```
Pretty much copy + paste view code...

### Actual behavior

A `FunctionClauseError` is raised.

```elixir
1) test index/2 returns a 200 if data is valid (API.V1.LayoutControllerTest)
     test/controllers/v1/layout_controller_test.exs:7
     ** (FunctionClauseError) no function clause matching in Phoenix.View.render_within/3

     The following arguments were given to Phoenix.View.render_within/3:

         # 1
         {%App.Layout{__meta__: #Ecto.Schema.Metadata<:loaded, "layouts">, image_url: nil, inserted_at: ~N[2018-07-17 18:43:48.046076], tags: [], title: "title", id: "layout-0", updated_at: ~N[2018-07-17 18:43:48.046086]}, %{}}

         # 2
         API.V1.LayoutView

         # 3
         "layout.json"

     Attempted function clauses (showing 2 out of 2):

         defp render_within({{layout_mod, layout_tpl}, assigns}, inner_mod, inner_tpl)
         defp render_within({false, assigns}, module, template)

     code: |> get("/layouts")
     stacktrace:
       (phoenix) lib/phoenix/view.ex:186: Phoenix.View.render_within/3
       (elixir) lib/enum.ex:1294: Enum."-map/2-lists^map/1-0-"/2
       (api) lib/api/views/v1/layout_view.ex:8: API.V1.LayoutView.render/2
       (phoenix) lib/phoenix/view.ex:332: Phoenix.View.render_to_iodata/3
       (phoenix) lib/phoenix/controller.ex:740: Phoenix.Controller.do_render/4
       (api) lib/api/controllers/v1/layout_controller.ex:1: API.V1.LayoutController.action/2
       (api) lib/api/controllers/v1/layout_controller.ex:1: API.V1.LayoutController.phoenix_controller_pipeline/2
       (api) lib/api/endpoint.ex:1: API.Endpoint.instrument/4
       (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
       (api) lib/api/endpoint.ex:1: API.Endpoint.plug_builder_call/2
       (api) lib/api/endpoint.ex:1: API.Endpoint.call/2
       (phoenix) lib/phoenix/test/conn_test.ex:224: Phoenix.ConnTest.dispatch/5
       test/controllers/v1/layout_controller_test.exs:13: (test)
```

The problem is that the `:layout` key is reserved.

This is an edge case, and there are easy workarounds to this, but I was am wondering if this could be caught beforehand.

Pull request description:
Validate reserved :layout field in View.render/3
Closes #2969

Patch:
diff --git a/lib/phoenix/view.ex b/lib/phoenix/view.ex
index e52636abd..f3fb09a23 100644
--- a/lib/phoenix/view.ex
+++ b/lib/phoenix/view.ex
@@ -241,23 +241,32 @@ defmodule Phoenix.View do
     |> render_within(module, template)
   end
 
-  defp render_within({{layout_mod, layout_tpl}, assigns}, inner_mod, inner_tpl) do
-    assigns = Map.merge(assigns, %{view_module: inner_mod,
-                                   view_template: inner_tpl})
-
-    render_layout(layout_mod, layout_tpl, assigns)
-  end
-
   defp render_within({false, assigns}, module, template) do
     assigns = Map.merge(assigns, %{view_module: module,
                                    view_template: template})
     module.render(template, assigns)
   end
 
-  defp render_layout(layout_mod, layout_tpl, assigns) do
+  defp render_within({layout, assigns}, inner_mod, inner_tpl) do
+    assigns = Map.merge(assigns, %{view_module: inner_mod, view_template: inner_tpl})
+    render_layout(layout, assigns)
+  end
+
+  defp render_layout({layout_mod, layout_tpl}, assigns)
+       when is_atom(layout_mod) and is_binary(layout_tpl) do
     layout_mod.render(layout_tpl, assigns)
   end
 
+  defp render_layout(layout, _assigns) do
+    raise ArgumentError, """
+    invalid value for reserved key :layout in View.render/3 assigns
+
+    :layout accepts a tuple of the form {LayoutModule, "template.extension"}
+
+    got: #{inspect(layout)}
+    """
+  end
+
   @doc """
   Renders a template only if it exists.
 


Test patch:
diff --git a/test/phoenix/view_test.exs b/test/phoenix/view_test.exs
index 02b4404ec..66ba16fc4 100644
--- a/test/phoenix/view_test.exs
+++ b/test/phoenix/view_test.exs
@@ -64,6 +64,16 @@ defmodule Phoenix.ViewTest do
                       [["" | "<div>Show! "] | "Hello world"] | "</div>\n"] | "</html>\n"]}
   end
 
+  test "validates explicitly passed layout" do
+    assert_raise ArgumentError, fn ->
+      render(MyApp.View, "show.html",
+        title: "Test",
+        message: "Hello world",
+        layout: {"not a layout", "app.html"}
+      )
+    end
+  end
+
   test "converts assigns to maps and removes :layout" do
     html = render_to_iodata(MyApp.UserView, "edit.html",
       title: "Test",