Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Creating Web Services with Liberator - f(by) 2016

Creating Web Services with Liberator - f(by) 2016

Slides for my conference talk at f(by) 2016 in Minsk

Liberator is a clojure liberator to create web services that fully embrace the REST architectural style. In this talk you will learn about the motivation to create liberator and how it's very declarative style allows you to build resources that strictly conform with the HTTP semantics and leverage the full potential of HTTP.

Philipp Meier

December 10, 2016
Tweet

More Decks by Philipp Meier

Other Decks in Programming

Transcript

  1. liberator A clojure library to implement REST resources https://clojure-liberator.github.io/ Philipp

    Meier @ordnungswprog Software Developer http://philipp.meier.name
  2. Clojure » lisp » repl » jvm » ClojureScript (targets

    javascript) » 10 years of awesomness in 2017
  3. data structures Immutable, Persistent » PersistentVector (aka. Bitmapped Vector Tries)

    » O(log32 n) is fast enough » Vectors, HashMaps, Sets
  4. Clojure syntax 101 (+ 2 (* 3 4)) ;; =>

    14 (fn [a b c] (+ a b c)) ;; => an anonymous fn / lambda (keep odd? [1 2 3 4]) ;; => [1 3]
  5. Clojure syntax 101 (def p {:firstname "Philipp" :lastname "Meier"}) ;;

    map destructuring (let [{f :firstname l :lastname} p] (str l ", " f)) ;; => "Meier, Philipp"
  6. Keywords examples :keyword (def person {:firstname "Philipp" :lastname "Meier"}) ;;

    keywords are functions to lookup from maps (:firstname person) ;; maps a functions to (person :firstname) ;; same as (get person :firstname) ;; nobody uses that
  7. Liberator Some History » New to clojure » Looking for

    a problem » Web applications since 1996 » Erlangs webmachine » This is Nov 2009 » Malcolm Sparks joins around Euroclojure 2012 » Getting speed...
  8. HTTP IS COMPLEX » Request and response » Headers and

    body » Methods » Status codes » Content negotiation » Conditional request » And more...
  9. HTTP Status codes 5 Classes » 1xx Informational » 2xx

    Successful » 3xx Redirection » 4xx Client Error » 5xx Server error
  10. HTTP Status codes 41 (< 41 2^6) » Maximum #

    of needed decisions » At least 40 branching points
  11. Ring Dispatch somehow gives status codes (defn my-handler [req] (cond

    ... {:status 503} ... {:status 501} ... {:status 403} ... {:status 405} ... {:status 400} ... {:status 200}))
  12. Ring Dispatch somehow gives status codes (defn my-handler [req] (cond

    ? {:status 503} ... {:status 501} ... {:status 403} ... {:status 405} ... {:status 400} ... {:status 200}))
  13. Ring Dispatch somehow gives status codes (defn my-handler [req] (cond

    ? {:status 503} ? {:status 501} ? {:status 403} ? {:status 405} ? {:status 400} ? {:status 200}))
  14. Implementing a well behaved ring handler - KISS (defn my-handler

    [req] {:status 200 :headers {"Content-Type" "text/html"} :body "a talk"})
  15. Implementing a well behaved ring handler - KISS (defn my-handler

    [{m :request-method {id "id"} :query-params}] (if (= :get m) {:status 200 :headers {"Content-Type" "text/plain; charset=UTF-8"} :body (pr-str (find-session id))} {:status 405 :body "Method not allowed."}))
  16. Implementing a well behaved ring handler Font size is getting

    smaller (defn my-handler [{m :request-method {id "id"} :params}] (if (= :get m) (if-let [e (find-session id)] {:status 200 :headers {"Content-Type" "text/plain; charset=UTF-8"} :body (pr-str (find-session id))} {:status 404 :body "Not found."}) {:status 405 :body "Method not allowed."}))
  17. Implementing a well behaved ring handler Can you still read

    this? (defn my-handler [{m :request-method {id "id"} :params {accept "accept"} :headers}] (if (= :get m) (if-let [e (find-session id)] (if-let [mt (some #(when (or (nil? accept) (= accept "*/*") (.contains accept %)) %) ["application/clojure" "text/html"])] (condp = mt "application/clojure" {:status 200 :headers {"Content-Type" "application/clojure; charset=UTF-8"} :body (pr-str e)} "text/html" {:status 200 :headers {"Content-Type" "text/html; charset=UTF-8"} :body (apply format "<html><h1>%s</h1><p>%s</p><p><small>%s</small></p></html>" ((juxt :title :time :speaker) e)) }) {:status 406 :body "Requested media type not acceptable."}) {:status 404 :body "Not found."}) {:status 505 :body "Method not allowed."}))
  18. What's going on? (defn handler [{m :request-method {id "id"} :params

    {accept "accept" ims "if-modified-since"} :headers}] (if (#{:get :post :put :delete :head :options :trace} m) (if (= :get m) (if-let [e (find-session id)] (if-let [mt (some #(when (or (nil? accept) (= accept "*/*") (.contains accept %)) %) ["application/clojure" "text/html"])] (let [last-modified (begin-of-last-minute) cache-headers {"Last-Modified" (.format (http-date-format) last-modified) "Vary" "Accept"}] (if (and ims (let [imsd (.parse (http-date-format) ims)] (= last-modified imsd))) {:status 304 :headers cache-headers} (condp = mt "application/clojure" {:status 200 :headers (assoc cache-headers "Content-Type" "application/clojure; charset=UTF-8") :body (pr-str e)} "text/html" {:status 200 :headers (assoc cache-headers "Content-Type" "text/html; charset=UTF-8") :body (apply format "<html><h1>%s</h1><p>%s</p><p><small>%s</small></p></html>" ((juxt :title :time :speaker) e)) }))) {:status 406 :body "Requested media type not acceptable."}) {:status 404 :body "Not found."}) {:status 501 :body "Not implemented."}) {:status 405 :body "Method not allowed."}))
  19. Conditionals! (defn handler [ #_... ] (if #_... (if #_...

    (if-let #_... (if-let #_... (if #_... {:status 304} (condp #_... {:status 200} {:status 200})) {:status 406}) {:status 404}) {:status 501}) {:status 405}))
  20. Demo result (defresource talk :available-media-types ["text/html" "application/json" "application/edn"] :exists? (fn

    [{{{id "id"} :params} :request}] (if-let [talk (find-talk id)] {::talk talk})) :handle-ok ::talk :last-modified (fn [_] (begin-of-last-minute)) :etag (fn [{{{id "id"} :params} :request}] (apply str (reverse (str id id)))))
  21. Remember the explicit implementation? (defn handler [{m :request-method {id "id"}

    :params {accept "accept" ims "if-modified-since"} :headers}] (if (#{:get :post :put :delete :head :options :trace} m) (if (= :get m) (if-let [e (find-session id)] (if-let [mt (some #(when (or (nil? accept) (= accept "*/*") (.contains accept %)) %) ["application/clojure" "text/html"])] (let [last-modified (begin-of-last-minute) cache-headers {"Last-Modified" (.format (http-date-format) last-modified) "Vary" "Accept"}] (if (and ims (let [imsd (.parse (http-date-format) ims)] (= last-modified imsd))) {:status 304 :headers cache-headers} (condp = mt "application/clojure" {:status 200 :headers (assoc cache-headers "Content-Type" "application/clojure; charset=UTF-8") :body (pr-str e)} "text/html" {:status 200 :headers (assoc cache-headers "Content-Type" "text/html; charset=UTF-8") :body (apply format "<html><h1>%s</h1><p>%s</p><p><small>%s</small></p></html>" ((juxt :title :time :speaker) e)) }))) {:status 406 :body "Requested media type not acceptable."}) {:status 404 :body "Not found."}) {:status 501 :body "Not implemented."}) {:status 405 :body "Method not allowed."}))
  22. Be more expressive (defresource talk :available-media-types ["text/html" "application/json" "application/edn"] :exists?

    (fn [{{{id "id"} :params} :request}] (if-let [talk (find-talk id)] {::talk talk})) :handle-ok (fn [{talk ::talk {mt :media-type} :representation}] (if (= mt "text/html" (apply format "<html><h1>%s</h1><p>%s</p><p><small>%s</small></p></html>" ((juxt :title :time :speaker) talk)) talk))) :last-modified (fn [_] (begin-of-last-minute)) :etag (fn [{{{id "id"} :params} :request}] (apply str (reverse (str id id)))))
  23. Application logic in the provided functions (defresource talk :available-media-types (fn

    [ctx] ...) :exists? (fn [ctx] ...) :handle-ok (fn [ctx] ...) :last-modified (fn [ctx] ...) :etag (fn [ctx] ...))
  24. Context Context is passed to all resource functions (defresource talk

    :available-media-types (fn [ctx] ...) :exists? (fn [ctx] ...) :handle-ok (fn [ctx] ...) :last-modified (fn [ctx] ...) :etag (fn [ctx] ...))
  25. Context update ; Boolean value false ; Map is merged

    -- and true {:talk {:title "liberator 101" ...}} ; Vector to specify bool and update [false {:error "Database explosion"}]
  26. Context Build incrementally {:request {:request-method :get :params ...} ::talk {:speaker

    "Philipp Meier"...} :representation {:media-type "application/edn" :language "de" :charset "UTF-8“}}
  27. Maps of functions (def talk (resource {:available-media-types (fn [ctx] ...)

    :exists? (fn [ctx] ...) :handle-ok (fn [ctx] ...) :last-modified (fn [ctx] ...) :etag (fn [ctx] ...)}))
  28. Implementation Macros for expressive code ;; name if-true if-false (defdecision

    authorized? allowed? handle-unauthorized) (defdecision malformed? handle-malformed authorized?) (defdecision method-allowed? coll-validator malformed? handle-method-not-allowed) (defdecision uri-too-long? handle-uri-too-long method-allowed?) (defdecision known-method? uri-too-long? handle-unknown-method) (defdecision service-available? known-method? handle-service-not-available) (defaction initialize-context service-available?)
  29. Implementation » Fixed (opinionated) decision graph » It's simply "functions

    calling functions" » Dispatched by function "decide" based on the return value of each decision function
  30. Decide (defn decide [name test then else {:keys [resource request]

    :as context}] (if (or test (contains? resource name)) (try (let [ftest (or (resource name) test) ftest (make-function ftest) fthen (make-function then) felse (make-function else) decision (ftest context) result (if (vector? decision) (first decision) decision) context-update (if (vector? decision) (second decision) decision) context (update-context context context-update)] (log! :decision name decision) ((if result fthen felse) context)) (catch Exception e (handle-exception (assoc context :exception e)))) {:status 500 :body (str "No handler found for key \"" name "\"." " Keys defined for resource are " (keys resource))}))
  31. Decide (defn decide [name then else {:keys [resource request] :as

    context}] (try (let [ftest (resource name) ftest (make-function ftest) fthen (make-function then) felse (make-function else) decision (ftest context) result (if (vector? decision) (first decision) decision) context-update (if (vector? decision) (second decision) decision) context (update-context context context-update)] (log! :decision name decision) ((if result fthen felse) context)) (catch Exception e (handle-exception (assoc context :exception e))))) ;;
  32. Call stack java.lang.ArithmeticException: Divide by zero at clojure.lang.Numbers.divide(Numbers.java:158) at clojure.lang.Numbers.divide(Numbers.java:3808)

    at free_your_data.liberator$talk$fn__22708.invoke(form-init6714364267415717902.clj:600) at liberator.core$gen_last_modified.invokeStatic(core.clj:62) at liberator.core$gen_last_modified.invoke(core.clj:60) at liberator.core$run_handler.invokeStatic(core.clj:146) at liberator.core$run_handler.invoke(core.clj:130) at liberator.core$handle_exception.invokeStatic(core.clj:495) at liberator.core$handle_exception.invoke(core.clj:495) at liberator.core$decide.invokeStatic(core.clj:88) at liberator.core$decide.invoke(core.clj:73) at liberator.core$initialize_context.invokeStatic(core.clj:493) at liberator.core$initialize_context.invoke(core.clj:493) at liberator.core$run_resource.invokeStatic(core.clj:581) at liberator.core$run_resource.invoke(core.clj:579) at free_your_data.liberator$talk.invokeStatic(form-init6714364267415717902.clj:592) at free_your_data.liberator$talk.invoke(form-init6714364267415717902.clj:592)
  33. Re: Essential complexity » You need to implement the decision

    functions no matter which technology. » With liberator thats often all you need to do.