Developing at the REPL
Introduction
Exploring your application at the REPL is fundamental to Clojure development. In the context of Pedestal applications, we often want to interrogate the routing system and invoke interceptors (or handlers) in isolation. These explorations often form the basis for more robust automated tests, which will be covered in the Unit testing guide.
What You Will Learn
After reading this guide, you will be able to perform the following tasks at the REPL:
-
Start and stop the HTTP server.
-
Reload your routes without restarting the server.
-
List your application’s routes.
-
Find a route by name.
-
Test the response of a single route.
-
Test for route recognition.
-
Test url generation.
-
Invoke an interceptor or handler. [TODO]
-
Examine the interceptor stack. [TODO]
-
Connect to a running application with Socket REPL. [TODO]
-
Capture the last request and examine it at the REPL. [TODO]
Guide Assumptions
This guide assumes you have a working REPL in your Pedestal application.
If you generated your application using the
pedestal-service Leiningen
template, this might be as simple as running lein repl
at the command
line. Perhaps your editor or IDE provides a facility for launching a REPL
for your project. A great resource for understanding and configuring your
REPL environment can be found at Lambda Island’s
Ultimate Guide to Clojure REPLs.
Getting Help if You’re Stuck
We’ll take this in small steps. If you get stuck at any point in this guide, please submit an issue about this guide or, hop over to the Pedestal Users mailing list and raise your hand there. You can also get help from the #pedestal channel on the Clojurians Slack.
Before We Begin
In this guide, we will develop a few simple utility functions that simplify interacting with your Pedestal application from the REPL. For the sake of having a consistent baseline setup, we’ll use a clean project for this guide. Generate a new Pedestal service using the following command:
lein new pedestal-service pedrepl; cd pedrepl
The default project template creates two files: server.clj
and
service.clj
.
We’re now ready to begin exploring the Pedestal application.
Ladies and gentlemen, start your REPLs!
Starting and stopping the HTTP server
The first thing we’ll want to do from the REPL is to control
the HTTP server. By default, your REPL should start in the
pedrepl.server namespace. The Leiningen template generates
a useful function named run-dev
which will configure and
start the HTTP server in development mode, preventing the
HTTP server thread from being joined and locking up your REPL.
In development mode, routes are reloaded when the service.clj
is re-required.
At the REPL, invoke the run-dev
function to start the
server:
pedrepl.server> (def serv (run-dev))
Now fire up a browser and navigate to http://localhost:8080 to see the default, "Hello, World!" message.
Within service.clj, update the home-page handler to respond with "Hello Pedestal User". Re-require/reload the service.clj, and when you reload http://localhost:8080 you’ll see the change immediately.
In dev-mode, you should never have to restart the server - changes are picked up as soon as they’re required/reloaded.
Should you ever need to stop the server, you can do so with the following invocation:
pedrepl.server> (server/stop serv)
Working with Routes
Now that we’re able to control the HTTP server, we’ll want some additional amenities. Next we’re going to add some route introspection utilities. Pedestal ships with all of the raw materials we need. Our job is simply to package these tools together into more convenient forms adapted to our specific needs.
Pedestal supports a few different route definition formats. Version
0.5.0 saw the advent of a new table format which is now the default
generated by the pedestal-service
Leiningen template. Let’s add a
reference to the table format namespace from server.clj. Your namespace
declaration should now look like this:
(ns pedrepl.server
(:gen-class)
(:require [io.pedestal.http :as server]
[io.pedestal.http.route :as route]
[io.pedestal.http.route.definition.table :refer [table-routes]] ; <-- new
[pedrepl.service :as service]
[clojure.java.io :as io]))
With that reference in place, we can add a new utility to print our application’s routes at the REPL:
(defn print-routes
"Print our application's routes"
[]
(route/print-routes (table-routes service/routes)))
Try this new function out at the REPL:
pedrepl.server> (print-routes)
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Method ┃ Path ┃ Name ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃ :get ┃ / ┃ :pedrepl.service/home-page ┃
┃ :get ┃ /about ┃ :pedrepl.service/about-page ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
nil
This simple output lists the HTTP method, relative path, and route name for each one of our routes. This is a great start, but as our application grows we may want to find a route by name. Let’s add another utility to do just that:
(defn named-route
"Finds a route by name"
[route-name]
(->> service/routes
table-routes
(filter #(= route-name (:route-name %)))
first))
Let’s test our new function:
pedrepl.server> (named-route ::service/home-page)
This function returns a map describing the home page
route in greater detail than print-routes
. Besides
the :path and :method keys, we’re given access to
the list of :interceptors that will be invoked when
this route is requested. It’s not uncommon for Pedestal
services to have quite a few interceptors, so the raw
output from named-route can get a little unwieldy. Let’s
see if we can produce some friendlier output.
Interceptors and Handlers
Interceptors are central to Pedestal applications. Not only do they provide the same pre- and post-processing of requests that Ring middleware does, but they also provide the main functionality behind every request. Everything in Pedestal is an interceptor, created by a single protocol - IntoInterceptor. When a route is compiled in Pedestal, it’s compiled with the full list of interceptors that run for that endpoint. It is often useful to see all interceptors that get run for a single endpoint. You can do that by looking through the route description or programmatically at the repl. Below is one way you might inspect and format that information.
(defn print-route
"Prints a route and its interceptors"
[rname]
(letfn [(joined-by
[s coll]
(apply str (interpose s coll)))
(repeat-str
[s n]
(apply str (repeat n s)))
(interceptor-info
[i]
(let [iname (or (:name i) "<handler>")
stages (joined-by
","
(keys
(filter
(comp (complement nil?) val)
(dissoc i :name))))]
(str iname " (" stages ")")))]
(when-let [rte (named-route rname)]
(let [{:keys [path method route-name interceptors]} rte
name-line (str "[" method " " path " " route-name "]")]
(joined-by
"\n"
(into [name-line (repeat-str "-" (count name-line))]
(map interceptor-info interceptors)))))))
Let’s use our new utility to print the ::service/home-page route information.
pedrepl.server> (print-route ::service/home-page)
This call produces the following output:
[:get / :pedrepl.service/home-page]
--------------------------------
:io.pedestal.http.body-params/body-params (:enter)
:io.pedestal.http/html-body (:leave)
<handler> (:enter)
Of course, this is a completely arbitrary representation of your route definition. Your own needs and aesthetic sensibilities should inform the structure of your application’s output.
Testing Route Responses
It’s not even necessary to start a server to see responses of your Pedestal service.
Pedestal ships with a response-for
function that will return the response of an
endpoint, exactly how it’s passed to the Servlet/Chain-Provider.
Let’s jump into the REPL:
pedrepl.server> (require '[io.pedestal.test :refer [response-for]])
nil
pedrepl.server> (def tempserv (::server/service-fn (server/create-servlet service/service)))
pedrepl.server> (response-for tempserv :get "/")
You can also use the same response-for
function to test against a live server.
Assuming you still have a running server from above (e.g. (def serv (run-dev))
),
You can do the following:
pedrepl.server> (response-for (::server/service-fn serv) :get "/")
Route Recognition and Generation
One of Pedestal’s benefits is that its routing table is bidirectional. Not only does Pedestal use the route definition to recognize routes based on request data, but it can also generate URLs to known route handlers. Let’s take a closer look at both of these capabilities in turn.
Route Recognition
When developing Pedestal applications, we often want to verify that
a given HTTP verb and relative path will invoke the correct handler.
Again, Pedestal provides all of the plumbing we need to answer these
questions from the comfort of the REPL. Add the following function
to server.clj
:
(defn recognize-route
"Verifies the requested HTTP verb and path are recognized by the router."
[verb path]
(route/try-routing-for (table-routes service/routes) :prefix-tree path verb))
With this function in place, we can test for route recognition from the REPL:
pedrepl.server> (recognize-route :get "/about")
=>
{:path "/about",
:method :get,
:path-re #"/\Qabout\E",
:path-parts ["about"],
:interceptors
[{:name :io.pedestal.http.body-params/body-params,
:enter #function[io.pedestal.interceptor.helpers/on-request/fn--8294],
:leave nil,
:error nil}
{:name :io.pedestal.http/html-body,
:enter nil,
:leave #function[io.pedestal.interceptor.helpers/on-response/fn--8311],
:error nil}
{:name nil, :enter #function[io.pedestal.interceptor/eval155/fn--156/fn--157], :leave nil, :error nil}],
:route-name :pedrepl.service/about-page,
:path-params {},
:io.pedestal.http.route.prefix-tree/satisfies-constraints? #function[clojure.core/constantly/fn--4614]}
Route Generation
When we generate urls during the lifetime of a request, we can simply
use Pedestal’s url-for
function.
However, this function relies on the url-for variable which is dynamically bound during the life of a
request. In order to test URL generation from the REPL, we’ll define
the following utility:
(defn dev-url-for
"Returns a url string for the named route"
[route-name & opts]
(let [f (route/url-for-routes (table-routes service/routes))
defaults {:host "localhost" :scheme :http :port 8080}
route-opts (flatten (seq (merge defaults (apply hash-map opts))))]
(apply f route-name route-opts)))
Let’s generate the URL for the about page:
pedrepl.server> (dev-url-for ::service/about-page)
=> "/about"
pedrepl.server> (dev-url-for ::service/about-page :absolute? true)
=> "http://localhost:8080/about"
Next Steps
In this guide we learned how to explore our Pedestal application from the REPL. Some of these explorations may serve as the basis for unit tests. Head over to the Unit testing guide to learn how to test the complete interceptor chain for a given route and much more.
Alternatively, you may want to learn to manage your applications stateful components (like the HTTP server) more effectively. Check out the guide on Using Pedestal With Component to learn how easy it can be to incorporate Stuart Sierra’s Reloaded workflow into your Pedestal development, and read Reloading at the REPL for a non-component take on how to make live changes without restarting.