By default, Sweet Tooth uses Liberator to construct request handlers. Liberator
provides an abstraction for the common decisions that a request handler has to
make, decisions like: Is this data valid? If not, return a 400 status. Is the
user authorized? If not, return a 401 status. The following two snippets
demonstrate two different approaches I’ve seen to handling this kind of logic
within the common use-case of creating a new resource:
typical endpoint code
(defn create-todo
[{:keys [params session] :as _req}]
(if-not (authorized? session)
(throw NotAuthorized "you do not have permission to do that"))
(if-let [errors (validation-errors ::create-todo params)]
{:status 400
:body {:errors errors}}
(create-todo! params)))
(defn create-todo
[{:keys [params session] :as _req}]
(cond (not (authorized? session))
{:status 401
:body {:errors "not authorized"}}
(not (valid? ::create-todo params))
{:status 400
:body {:errors (validation-errors ::create-todo params)}}
:else
(create-todo! params)))
The structure of this if/then logic to determine which status to return is
always ways the same. You’ll always want to check whether the user is authorized
before attempting to validate data, and you’ll always want to validate data
before attempting to insert it in a db. If the request isn’t authorized, you
should always return a 401 status, and if it’s not valid you should return a 400
status. You can visualize this structure as a decision graph:
Liberator provides a function that captures the the decision graph — the order
that the decisions are traversed and the status codes associated with each final
result — so that your code only has to focus on the logic specific to each
decision.
Below is a simplified version of that function. It takes a map of decision
functions and returns a request handler which can be used to return responses
for ring requests.
|
Liberator uses :malformed? instead of :valid? and :handle-ok instead
of :success because it’s actually structuring decisions regarding how to
return a valid HTTP response, and the language of malformed and ok hews more
closely to the HTTP spec.
|
simplified decision request generator
(def decision-graph
{:authorized? {true :malformed?
false 401}
:malformed? {true 400
false :handle-ok}
:handle-ok 200})
(defn decisions->handler
[decision-nodes]
(fn request-handler [req]
(loop [node :authorized?]
(let [result ((node decision-nodes) req)
edges-or-status (node decision-graph)
next-node-or-status (get edges-or-status (boolean result) edges-or-status)]
(if (keyword? next-node-or-status)
(recur next-node-or-status) ;; it was a node; on to the next decision!
{:status next-node-or-status
:body result})))))
The code might be a little dense and I welcome suggestions clearer :)
decisions→handler
returns a function that traverses decision-graph
,
starting with the :authorized?
node. As it traverses the graph, it looks up
decision functions in decision-nodes
. It uses the return value of the decision
function to retrieve either the next decision node or the response status. If a
status is retrieved, the function returns.
Here’s how you would call this function to create a handler:
create and call a handler
(def handler (decisions->handler {:authorized? (constantly true)
:malformed? (constantly false)
:handle-ok (constantly "hi")}))
(handler {})
;; =>
{:status 200, body "hi"}
|
The simplified Liberator re-implementation differs from actual Liberator in a
couple important ways. First, the real library provides sane defaults for
:authorized? , :malformed? and all the rest of its decision functions. I
didn’t include default handling in these examples to keep the code more focused.
Second, Liberator allows you to provide constants, so you can write
:authorized? true instead of :authorized? (constantly true) and :handle-ok
"hi" instead of :handle-ok (constantly "hi")) .
|