(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))}))