aboutsummaryrefslogtreecommitdiffstats
path: root/src/gallifrey/handler.clj
blob: 61bdd16ca5b7de6acac17113ba0b12b6212ae30c (plain)
1
(ns gallifrey.handler
  (:require [gallifrey.store :as store]
            [utilis.map :refer [map-vals compact]]
            [utilis.fn :refer [fsafe apply-kw]]
            [liberator.core :refer [defresource]]
            [liberator.representation :refer [as-response ring-response]]
            [compojure.core :refer [GET PUT PATCH ANY defroutes]]
            [compojure.route :refer [resources]]
            [ring.util.response :refer [resource-response content-type]]
            [ring.middleware.defaults :refer [wrap-defaults api-defaults]]
            [ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
            [ring.middleware.session :refer [wrap-session]]
            [cheshire.core :as json]
            [clj-time.format :as tf]
            [metrics.ring.instrument :refer [instrument]]
            [metrics.ring.expose :refer [expose-metrics-as-json]]
            [integrant.core :as ig]))

(declare handler)

(defonce ^:private the-store (atom nil))

(defmethod ig/init-key :gallifrey/handler  [_ {:keys [store]}]
  (reset! the-store store)
  handler)

(defmethod ig/halt-key! :gallifrey/handler  [_ _]
  (reset! the-store nil))

(declare parse-ts serialize serialize-entity serialize-relationship)

(defn entity-existed?
  [type id & {:keys [t-k]}]
  (store/entity-existed? @the-store type id :t-k t-k))

(defresource entity-endpoint [type id]
  :allowed-methods [:get :put :delete]
  :available-media-types ["application/json"]
  :malformed? (fn [ctx]
                (when (#{:put :delete} (-> ctx :request :request-method))
                  (-> ctx :request :params :actor empty?)))
  :exists? (fn [ctx]
             (if-let [resource (store/get-entity @the-store type id
                                                 :t-t (parse-ts ctx :t-t) :t-k (parse-ts ctx :t-k))]
               {::resource ((case type
                              "entity" serialize-entity
                              "relationship" serialize-relationship) resource)}
               (when (and (= :put (-> ctx :request :request-method))
                          (-> ctx :request :params :create not-empty))
                 true)))
  :existed? (fn [ctx] (entity-existed? type id :t-k (parse-ts ctx :t-k)))
  :handle-ok ::resource
  :can-put-to-missing? false
  :handle-not-implemented (fn [{{m :request-method} :request :as ctx}]
                            (when (= :put m)
                              (-> (as-response "Resource not found" ctx)
                                  (assoc :status (if (entity-existed? type id) 410 404))
                                  ring-response)))
  :put! (fn [ctx]
          (let [body (json/parse-string (slurp (get-in ctx [:request :body])))
                properties (get body "properties")
                source {"source.type" (get-in body ["source" "type"])
                        "source.id" (get-in body ["source" "id"])}
                target {"target.type" (get-in body ["target" "type"])
                        "target.id" (get-in body ["target" "id"])}
                actor (-> ctx :request :params :actor)
                changes-only (boolean (-> ctx :request :params :changes-only))]
            {::created? (:created? (store/put-entity @the-store actor type id
                                                     (compact (merge properties source target))
                                                     :t-t (parse-ts ctx :t-t)
                                                     :changes-only changes-only))}))
  :delete! (fn [ctx]
             (store/delete-entity @the-store (-> ctx :request :params :actor) type id))
  :new? ::created?)

(defresource entity-history-endpoint [type id]
  :allowed-methods [:get]
  :available-media-types ["application/json"]
  :exists? (fn [ctx]
             (when-let [resource (not-empty
                                  (store/entity-history @the-store type id))]
               {::resource (map-vals (partial map #(-> %
                                                       (update :k-start str)
                                                       (update :k-end str)
                                                       (update :t-start str)
                                                       (update :t-end str)
                                                       compact)) resource)}))
  :handle-ok ::resource)

(defresource entity-lifespan-endpoint [type id]
  :allowed-methods [:get]
  :available-media-types ["application/json"]
  :exists? (fn [ctx]
             (when-let [resource (not-empty
                                  (store/entity-lifespan @the-store type id))]
               {::resource (map #(-> %
                                     (update :created str)
                                     (update :updated (partial map str))
                                     (update :deleted str)
                                     compact) resource)}))
  :handle-ok ::resource)

(defresource entity-aggregation-endpoint [type]
  :allowed-methods [:get]
  :available-media-types ["application/json"]
  :handle-ok #(store/aggregate-entities @the-store type
                                        :filters (->> (dissoc (get-in % [:request :params])
                                                              :properties :t-t :t-k
                                                              :gallifrey-type)
                                                      (map (fn [[k v]]
                                                             (cond
                                                               (= "null" v) [k nil]
                                                               :else [k v])))
                                                      (into {}))
                                        :properties (get-in % [:request :params :properties])
                                        :t-t (parse-ts % :t-t) :t-k (parse-ts % :t-k)))

(defroutes app-routes
  (GET "/:gallifrey-type/aggregations" [gallifrey-type] (entity-aggregation-endpoint gallifrey-type))
  (ANY "/:gallifrey-type/:id" [gallifrey-type id] (entity-endpoint gallifrey-type id))
  (GET "/:gallifrey-type/:id/history" [gallifrey-type id] (entity-history-endpoint gallifrey-type id))
  (GET "/:gallifrey-type/:id/lifespan" [gallifrey-type id] (entity-lifespan-endpoint gallifrey-type id))
  (resources "/"))

(def handler
  (-> app-routes
      (wrap-defaults api-defaults)
      instrument
      expose-metrics-as-json))


;;; Implementation

(defn- parse-ts
  [ctx key]
  (when-let [ts (get-in ctx [:request :params key])]
    (tf/parse ts)))

(defn- serialize-relationship
  [resource]
  (compact
   {:properties (dissoc resource :_id :_meta
                        :source.type :source.id
                        :target.type :target.id)
    :source {:type (:source.type resource)
             :id (:source.id resource)}
    :target {:type (:target.type resource)
             :id (:target.id resource)}
    :_id (:_id resource)
    :_meta (map-vals (partial map-vals str) (:_meta resource))}))

(defn- serialize-entity
  [resource]
  (compact
   {:properties (dissoc resource :_id :_meta :_relationships)
    :relationships (map serialize-relationship (:_relationships resource))
    :_id (:_id resource)
    :_meta (map-vals (partial map-vals str) (:_meta resource))}))