src/jiksnu/modules/web/helpers.clj
(ns jiksnu.modules.web.helpers
(:require [clojure.java.io :as io]
[clojure.tools.reader.edn :as edn]
[hiccup.core :as h]
[jiksnu.model.user :as model.user]
[jiksnu.modules.as.helpers :as as.helpers]
[jiksnu.modules.core.actions :as actions]
[jiksnu.modules.core.helpers :as core.helpers]
[jiksnu.modules.http.resources :refer [defresource]]
[jiksnu.modules.web.sections.layout-sections :as sections.layout]
[jiksnu.registry :as registry]
[jiksnu.session :as session]
[octohipster.mixins :as mixin]
[slingshot.slingshot :refer [throw+]]
[taoensso.timbre :as timbre])
(:import java.io.FileNotFoundException
java.io.PushbackReader
org.apache.http.HttpStatus))
(def types
{:json "application/json"
:html "text/html"})
(defonce parameters (ref {}))
(defn defparameter
[k & {:as options}]
(dosync
(alter parameters assoc k options)))
(defn get-parameter
[k]
(k @parameters))
(defn path
[k]
(merge (get-parameter k) {:in "path"}))
(defn index
[ctx]
(sections.layout/page-template-content (:request ctx) {}))
(defn angular-resource-handle-ok
[{{method :request-method} :request
resource :resource :as ctx}]
(let [k (keyword (str "handle-ok-" (name method)))]
(timbre/infof "Method: %s" method)
((get resource k index) ctx)))
(defn angular-resource-description
[r]
{:summary (or (when-let [state (get-in r [:methods :get :state])]
(str "state: " state))
"Angular Template")
:description "This is a double for an angular route. Requesting this page directly will return the angular page."
:contentType "text/html"
:responses
{"200" {:description (or (get-in r [:methods :get :description])
(:description r)
"Angular Template")
:schema {:type "string"}}}})
(defn load-group
[group]
(let [route-sym (symbol (format "jiksnu.modules.web.routes.%s-routes" group))]
(if (try (require route-sym) true (catch FileNotFoundException _ nil))
(do
(timbre/with-context {:sym (str route-sym)}
(timbre/debugf "Loading route group - %s" route-sym))
(try
(core.helpers/load-pages! route-sym)
(core.helpers/load-sub-pages! route-sym)
(catch Exception ex
(timbre/error ex "Failed to load routes")
(throw+ ex))))
(timbre/warnf "Could not require group - %s" group))))
(defn load-routes
[]
(doseq [group registry/action-group-names]
(load-group group)))
(defn serve-template
[{{template-name :*} :params}]
(let [path (str "templates/" template-name ".edn")]
(if-let [data (some-> path io/resource io/reader PushbackReader. edn/read)]
{:headers {"Content-Type" "text/html"}
:body (h/html data)
:status HttpStatus/SC_OK}
{:status HttpStatus/SC_NOT_FOUND})))
(defn get-handler
[ctx handler-sym]
(if-let [action-ns-sym (:ns (:resource ctx))]
(if-let [model-ns-var (some-> (action-ns-sym) (ns-resolve 'model-ns))]
(if-let [model-ns-ref (var-get model-ns-var)]
(or (ns-resolve model-ns-ref handler-sym)
(throw+ {:message "Could not determine handler sym" :handler handler-sym}))
(throw+ {:message "Model ns not found" :var model-ns-var}))
(throw+ {:message "Model ns not defined" :action-ns (action-ns-sym)}))
(throw+ {:message "Could not determine action namespace"})))
(defn page-exists?
[{resource :resource}]
(if-let [page-name (when-let [page-name-fn (:page resource)] (page-name-fn))]
(do
(timbre/with-context {:page page-name}
(timbre/debugf "Fetching Page - %s" page-name))
{:data (actions/get-page page-name)})
(throw+ {:message "Resource does not define a page name"})))
(defn subpage-exists?
[{{:keys [subpage target target-model]} :resource
{{id :_id} :route-params} :request :as ctx}]
(timbre/infof "fetching subpage - %s(%s)" target-model subpage)
(when-let [item (actions/get-model (target-model) id)]
{:data (actions/get-sub-page item (subpage))}))
(defn get-user
"Gets the user from the context"
[{{{username :username} :route-params} :request}]
(model.user/get-user username))
(defn item-resource-delete!
"Generic item delete handler for page items"
[ctx]
(let [{{{id :_id} :route-params} :request} ctx]
((get-handler ctx 'delete) (:data ctx))))
(defn item-resource-malformed?
[ctx]
(if-let [fetcher (get-handler ctx 'fetch-by-id)]
(if-let [id (get-in ctx [:request :route-params :_id])]
[false {:data (fetcher id)}]
true)
(throw+ {:message "Could not determine fetcher"})))
(defn item-resource-authorized?
[ctx]
(let [{{method :request-method} :request} ctx
username (session/current-user-id)]
[(if (#{:put :delete} method) (boolean (seq username)) true)
{:username username}]))
;;; Resource Mixins
(defn ciste-resource
"route mixin for paths that use ciste"
[{:keys [available-formats] :as resource}]
(-> {:available-media-types (mapv types available-formats)}
(merge resource)
mixin/handled-resource))
(defn page-resource
"route mixin for paths that operate on a page"
[{action-ns :ns :as resource}]
(if-let [action-sym (ns-resolve action-ns 'index)]
(-> {:allowed-methods [:get :post :delete]
:available-formats [:json]
:available-media-types ["application/json"]
:exists? page-exists?
;; FIXME: Return actual count
:count (constantly 4)}
(merge resource)
ciste-resource)
(throw+ "Could not resolve index action")))
(defn item-resource
"Route mixin for resources that represent a page item"
[resource]
(if-let [action-ns-sym (:ns resource)]
(do
(require action-ns-sym)
(-> {:respond-with-entity? false
:allowed-methods [:get :put :delete]
:available-formats [:json :clj]
:methods {:get {:summary "Index Item"
:contentType "application/json"
:description "view of an item"
:responses {"200" {:description "Valid response"
:schema {:type "string"}}}}}
:exists? :data
:malformed? item-resource-malformed?
:delete! item-resource-delete!
:authorized? item-resource-authorized?}
(merge resource)
ciste-resource
mixin/item-resource))
(throw+ {:message "Ns not defined"})))
(defn subpage-resource
"route mixin for paths that operate on a subpage"
[resource]
(-> {:allowed-methods [:get]
:available-formats [:json]
:exists? subpage-exists?}
(merge resource)
ciste-resource))
(defn as-collection-resource
[{:keys [indexer fetcher collection-type] :as resource}]
(-> {:allowed-methods [:get :post]
:available-media-types ["application/json"]
:can-put-to-missing? false
:methods {:get {:summary (str "Get Collection of " collection-type)}
:post {:summary (str "Add to collection of " collection-type)}}
:collection-key :collection
:exists? (fn [ctx]
(let [user (get-user ctx)
page (-> (indexer ctx user)
(assoc :objectTypes collection-type)
(update :items #(map fetcher %)))]
{:data (as.helpers/format-collection user page)}))
:new? false
:parameters {:username (path :model.user/username)}
:respond-with-entity? true
:schema {:type "object"}}
(merge resource)
mixin/handled-resource))
(defn angular-resource
[{:keys [methods] :as r}]
(let [get-method (:get methods)
description (merge (angular-resource-description r) get-method)]
(-> {:exists? true
:handle-ok angular-resource-handle-ok
:available-media-types (mapv types [:html])}
(merge r)
(assoc-in [:methods :get] description))))