Patterns & Pitfalls in
Liberator

David James Humphreys (Juxter) [@davidjhumphreys]
/davidjameshumphreys/patterns-pitfalls-liberator/ [slides]

What is Liberator?

/Clojure-liberator/liberator

A brilliant library that follows the HTTP graph

  • Hook in to the places you need
  • Sensible defaults for the others
  • So...

... just find the parts that you need to hook in.

Flexibility vs. Flexibility

There is a lot to configure to set up a basic route.

  • It follows the HTTP spec closely (very, very closely)
  • Gives fine-grained control over each step
  • Map of functions
  • Return maps of data to chain to the next step in the graph
  • A lot of preamble to get going*

* if you are really impatient!

Chaining the data

The returned map is merged into the functions that occur afterwards.*


(def simple-get
  (resource {:available-media-types ["text/html"]
             ;; do your db call in here
             :exists?               (fn [ctx] {:data "My data"})
             ;; the keys are merged in
             :handle-ok             (fn [{:keys [data] :as ctx}]
                                      data)}))
      

* with a few exceptions and ways to prevent this.

Some patterns

  • Always Be Coercing
  • Accepting & Output Types
  • GET/POST patterns
  • Databases & resources on the request
  • Authentication
  • Build your own patterns
  • Use (fn render [args])

A Basic Get


(defn- json? [ctx]
  (-> ctx
      :representation
      :media-type
      (= "application/json")))

(def simple-get-with-type
  (resource {:available-media-types ["text/html" "application/json"]
             :allowed-methods       [:get]
             :exists?               (fn exists [ctx] {:data "My data"})
             :handle-ok             (fn ok [{:keys [data] :as ctx}]
                                      (if (json? ctx)
                                        {:data data
                                         :is-json true}
                                        data))}))
      

Coercing:

  • Use some library to coerce input data into the correct format
  • Use it everywhere (path- & query-params)
  • All input types form-encoded, JSON &c*
  • There are so many bad things one can do with input

/Prismatic/schema is a great choice

*Use some middleware to make coercion easier

Schema/coerce & Liberator


(defn make-malformed-coercer
  "A wrapper for checking the malformed state using Schema coercers.
  Liberator malformed expects [malformed? {:some data}] to be returned
  from the function."
  [coerce-fn]
  (fn malformed? [ctx]
    (let [result (coerce-fn ctx)]
      (log/info result)
      (if (error? result)
        [true (merge result
                     (try (negotiate-media-type ctx)
                          (catch ProtocolException _
                            {:representation
                              {:media-type "application/json"}})))]
        [false {:coerced-params result}]))))
      

little hack to render a result

Define your patterns


(defn do-get
  "A simple get endpoint."
  [check-vals-coercer exists? render & {:as overrides}]
  (merge {:available-media-types
              ["text/html" "application/json"]
          :allowed-methods [:get]
          :malformed?
              (make-malformed-coercer check-vals-coercer)
          :exists?         exists?
          :handle-ok       render}
         overrides))
      

Using it


(resource
  (do-get
    (fn [ctx] (-> ctx
                  :request
                  :params
                  (coercer/coerce {:expected schema/Str} {mapping})))
    (fn exists? [{:keys [request]}]
      {:data (get-data (:database request))})
    render-fn))
      

A basic Post

Using a similar pattern


(defn do-post
  "A simple post endpoint."
  [check-vals-coercer exists? post! post-redirect? & {:as overrides}]
  (merge post
         {:malformed?     (make-malformed-coercer check-vals-coercer)
          :exists?        exists?
          :post!          post!
          :post-redirect? post-redirect?}
         overrides))
      

Databases & other resources

Add all of your context-sensitive references into request

  • Environmental settings
    • Databases
    • Queues
    • Render settings
  • Bidi!!!!!!1111one

Use middleware to do it.

Middleware

No *database* globals please


(defn the-application
  "All of the webapp routes and middleware. By defining in this way we
   can pass in various settings (i.e. to add a database create a middleware
   to wrap the request)"
  [component-settings]
  (-> #'server-routes
      (wrap-trace :ui true)  ;; <- liberator trace
      (wrap-renderer (-> component-settings :render :global-vars))
      (wrap-database (-> component-settings :database))
      wrap-keyword-params    ;; <- param helpers for schema validation
      wrap-params
      wrap-json-params
      (wrap-bidi-handlers build-routes handlers)))
      

Maybe use components

Pitfalls of Flexibility

  • Sensible* defaults
  • Headers?
    • Access-Control-Allow-Origin
    • Cache-Control
    • Sessions & Cookies
    • &c.

Pitfalls of the graph

service-available? happens first, what about long requests?

:as-response (fn [this ctx] build-ring-response)

Other libraries

Fin)

/davidjameshumphreys/patterns-pitfalls-liberator/ [slides]