Skip to content

Instantly share code, notes, and snippets.

@jwietelmann
Created August 31, 2016 17:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jwietelmann/8c13e6be8009e2a6ff0ab2fcec520907 to your computer and use it in GitHub Desktop.
Save jwietelmann/8c13e6be8009e2a6ff0ab2fcec520907 to your computer and use it in GitHub Desktop.
A rough sketch of a Phoenix router that uses an independent rendering service to create HTML from JSON views.
# This plug takes HTML requests, forwards them to our controllers as JSON
# requests, takes the resulting JSON and POSTs it to a rendering service,
# then transforms the response back into HTML using the output from the
# rendering service.
#
# Which means we can write our probably-JavaScript/React view layer
# independently of Phoenix tooling.
defmodule ServiceRenderer do
# Base URL of render service goes here.
def init(url) do
url
end
# Apply to HTML requests only.
def should_apply?(conn) do
accept_header = List.first(Plug.Conn.get_req_header(conn, "accept")) || ""
String.contains?(accept_header, "text/html")
end
# Change the HTML request so it will be processed first as JSON.
def change_format_to_json(conn) do
Plug.Conn.put_private(conn, :phoenix_format, "json")
end
# Register the response transformation handler that will call the service.
def register_before_send(conn, url) do
hook = fn(conn) ->
before_send(conn, url)
end
Plug.Conn.register_before_send(conn, hook)
end
# Success. Replace the body and set the correct content type.
def handle_service_response(conn, {:ok, %HTTPoison.Response{status_code: 200, body: body}}) do
%{conn | resp_body: body} |> Plug.Conn.put_resp_header("Content-Type", "text/html")
end
# The service didn't have a view for us.
def handle_service_response(conn, {:ok, %HTTPoison.Response{status_code: 404}}) do
raise "Not Found."
end
# The service sent an unexpected or error code.
def handle_service_response(conn, {:ok, %HTTPoison.Response{status_code: status_code}}) do
conn
end
# Something bad happened.
def handle_service_response(conn, {:error, %HTTPoison.Error{reason: reason}}) do
raise "Error."
end
# The response transformation.
def before_send(conn, url) do
body = to_string(conn.resp_body)
headers = [{"Content-Type", "application/json"}]
service_response = HTTPoison.post(url, body, headers)
handle_service_response(conn, service_response)
end
# Plug API
def call(conn, url) do
if should_apply?(conn) do
conn |> change_format_to_json |> register_before_send(url)
else
conn
end
end
end
# Phoenix won't let us forward to the same router in different scopes.
# So I made this macro to let us apply the same routing rules to
# different routers.
defmodule Turbophoenix.UIRouter do
defmacro __using__(_opts) do
quote do
use Turbophoenix.Web, :router
scope "/", Turbophoenix do
# Define UI routes here
get "/", PageController, :index
end
end
end
end
# We'll forward all /jsonui prefixed paths to this one.
defmodule Turbophoenix.JSONRouter do
use Turbophoenix.UIRouter
end
# We'll forward the rest to this one.
defmodule Turbophoenix.HTMLRouter do
use Turbophoenix.UIRouter
end
defmodule Turbophoenix.Router do
use Turbophoenix.Web, :router
pipeline :htmlui do
plug :put_layout, false
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug ServiceRenderer, "http://localhost:8000"
end
pipeline :jsonui do
plug :put_layout, false
plug :accepts, ["json"]
end
# Any path prefixed with /jsonui will send raw JSON.
scope "/jsonui", Turbophoenix do
pipe_through :jsonui
forward "/", JSONRouter
end
# Any other path will pump JSON through our rendering service to get the HTML.
scope "/", Turbophoenix do
pipe_through :htmlui
forward "/", HTMLRouter
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment