On Github ordnungswidrig / clojure-conj-2015-i-did-the-api-wrong
clojure/conj Philadelphia, PA 2015
Philipp Meier – mailto:philipp@meier.name – @ordnungswprog
you can create quite some mess
_
That escalated quickly
Liberator knobs _
You must guide your users
_
(defresource frobnicator [db id]
  :exists           (fn [ctx] {::frob (load-from-db db id)})
  :e-tag            (fn [{frob ::frob}] (:_version frob))
  :handle-not-found (fn [ctx] (format "Frob not found with id %s" id))
  :handle-ok        (fn [{frob ::frob} frob]))
(defresource frobnicator [db id]
  :exists?          (fn [ctx] {::frob (load-from-db db id)})
  :e-tag            (fn [{frob ::frob}] (:_version frob))
  :handle-not-found (fn [ctx] (format "Frob not found with id %s" id))
  :handle-ok        (fn [{frob ::frob} frob]))
…do not compose too well
(defn load-entity [db & {:as opts}] ...)
(def default-opts {:eager true :keywordize false :fetch-depth 5})
(defn load-customer [db id]
  (apply load-entity db (merge default-ops :query {:id id}))
;; => IllegalArgumentException No value supplied for key: [:fetch-depth 5] clojure.lang.PersistentHashMap.create (PersistentHashMap.java:77)
(apply load-entity my-db (apply concat (merge default-ops :query {:id id}))
(->> some-opts (apply concat) (apply load-entitiy))
_
(defresource foo :handle-ok "ok")
(def defaults {:exists? (fn [_] (zero? (rand-int 1)))})
(apply defresource bar (merge defaults {:handle-ok "ok"}))
;; => CompilerException java.lang.RuntimeException: Can't take value of a macro: #'liberator.core/defresource, compiling:(/private/var/folders/p1/47jm9vq12g93ry9vf525h7vh0000gn/T/form-init2406362871536982238.clj:1:1)
find backward compatible way
optional map as first argument
(defresource bar defaults {:handle-ok "ok"})
better stay away from macros
(def bar (resource defaults :handle-ok "ok"))
(def bar (resource (merge defaults {:handle-ok "ok"}))
(fn [ctx]
 (ring-response "response-body" {:headers {"X-Foo" "bar"}))))
But this is what you need to do
(fn [ctx]
 ;; workaround until issue #152 is fixed
 (-> "response-body"
     (as-response (assoc-in ctx [:representation :media-type] "text/plain"))
     (assoc-in [:headers "X-Foo"] "bar")
     (ring-response))))
(defn print-my-stuff [stuff] (println (str "Stuff: " (.toString stuff))
;; => NullPointerException clojure.lang.Reflector.invokeNoArgInstanceMember (Reflector.java:301)
Reflector.java?
500 INTERNAL SERVER ERROR
;; since 0.12.0
(defresource foo
  :handle-exception (fn [{e :exception :as context}]
                      ...) ;; hurray
  ...)
State
State?
State!
State is part of the API
(def system-state (atom {})
(def another-state (atom {})
(def auxiliary-state (atom {})
(def state-manager-state (atom {})
(def yetanother-state (atom {})
(def state (atom {:system-state {}
                  :another-state {}
                  :auxiliary-state {}
                  :yetanother-state {}})
*-in is your friend
(defn start-ingest [state file] (start-ingest-in-background file) (assoc-in state [:system-state :ingest :status] :running)) (swap! state start-ingest file)
(def ^:dynamic db) (with-db (connect some-url) (fetch-from-db some-db))
(defn exec [db query] ...)
(defn fetch [db entity id]
  (exec db (build-query entity id)))
(defn load-user [db id]
  (fetch db :user id))
(defn authorized? [db id role]
  (let [user (load-user db id)]
    ...))
You cannot bind a dynamic var to two values
I want to talk about fixing all this on a more general level
Not function docstrings
;; request
{:request-method :get :headers {...}}
;; response
{:status 418 :body "I'm a teapot" :headers {...}}
;; request handler
(fn [req]
  {:status 418 :body "I'm a teapot" :headers {...}})
;; this is passed as the context
{:status ...
 :request ...
 :representation ...
 :resource ...
 :whatever-you-want ...}
;; every decision function
(fn [ctx]
  (do-something)
  {:foo "some value"} ;; context update
;; context is "merged" with context update
(Insert applause jingle here)
_
(defprotocol Resource (exists? [ctx] "returns true when the resource currently exists") (existed? [ctx] "returns true when the resoues previosly existed") (handle-ok [ctx] "returns the representation of resource entity") ...)
Yada
(defrecord StringResource [s last-modified]
  Representations
  (representations [_]
    [{:media-type "text/plain" :charset platform-charsets}])
  ResourceModification
  (last-modified [_ _] last-modified)
  ResourceVersion
  (version [_ _] s)
  Get
  (GET [_ _] s))
om-next does this
nice experiment take the users perspective
WORKSASDESIGNED - learn why a use wants this
Trapdoor: English Lock at en.wikipedia [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], from Wikimedia Commons https://upload.wikimedia.org/wikipedia/commons/f/fb/Trapdoor.jpg