Separated caching into new namespace.

This commit is contained in:
Simon Brooke 2018-09-30 14:33:51 +01:00
parent da5d884605
commit 2df3444090
4 changed files with 391 additions and 255 deletions

View file

@ -1,7 +1,8 @@
(ns ^{:doc "Application Description Language - command line invocation." (ns ^{:doc "Application Description Language - command line invocation."
:author "Simon Brooke"} :author "Simon Brooke"}
adl.main adl.main
(:require [adl.to-hugsql-queries :as h] (:require [adl.to-cache :as c]
[adl.to-hugsql-queries :as h]
[adl.to-json-routes :as j] [adl.to-json-routes :as j]
[adl.to-psql :as p] [adl.to-psql :as p]
[adl.to-selmer-routes :as s] [adl.to-selmer-routes :as s]
@ -52,8 +53,7 @@
:default "generated"] :default "generated"]
["-v" "--verbosity [LEVEL]" nil "Verbosity level - integer value required" ["-v" "--verbosity [LEVEL]" nil "Verbosity level - integer value required"
:parse-fn #(Integer/parseInt %) :parse-fn #(Integer/parseInt %)
:default 0] :default 0]])
])
(defn usage (defn usage
@ -105,6 +105,7 @@
#(if #(if
(.exists (java.io.File. %)) (.exists (java.io.File. %))
(let [application (x/parse (canonicalise %))] (let [application (x/parse (canonicalise %))]
(c/to-cache application)
(h/to-hugsql-queries application) (h/to-hugsql-queries application)
(j/to-json-routes application) (j/to-json-routes application)
(p/to-psql application) (p/to-psql application)

124
src/adl/to_cache.clj Normal file
View file

@ -0,0 +1,124 @@
(ns ^{:doc "Application Description Language: generate caching layer for database requests."
:author "Simon Brooke"}
adl.to-cache
(:require [adl-support.core :refer :all]
[adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [generate-documentation queries]]
[clj-time.core :as t]
[clj-time.format :as f]
[clojure.java.io :refer [file make-parents writer]]
[clojure.pprint :refer [pprint]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; adl.to-cache: generate caching layer for database requests.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2018 Simon Brooke
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; You can't cache the actual HugSQL functions (or at least, I don't know how
;;; you would); there's no point caching JSON requests because the request data
;;; will be different every time.
;;; The overall structure of this has quite closely to follow the structure of
;;; to-hugsql-queries, because essentially we need one JSON entry point to wrap
;;; each query.
;;; TODO: memoisation of handlers probably doesn't make sense, because every request
;;; will be different. I don't think we can memoise HugSQL, at least not without
;;; hacking the library (might be worth doing that and contributing a patch).
;;; So the solution may be to an intervening namespace 'cache', which has one
;;; memoised function for each hugsql query.
(defn file-header
"Generate an appropriate file header for JSON routes for this `application`."
[application]
(list
'ns
(symbol (str (safe-name (:name (:attrs application))) ".cache"))
(str "Caching wrappers for queries for " (:name (:attrs application))
" auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at "
(f/unparse (f/formatters :basic-date-time) (t/now)))
(list
:require
'[adl-support.core :refer :all]
'[adl-support.rest-support :refer :all]
'[clojure.core.memoize :as memo]
'[clojure.java.io :as io]
'[clojure.tools.logging :as log]
'[compojure.core :refer [defroutes GET POST]]
'[hugsql.core :as hugsql]
'[noir.response :as nresponse]
'[noir.util.route :as route]
'[ring.util.http-response :as response]
(vector (symbol (str (safe-name (:name (:attrs application))) ".db.core")) :as 'db))))
(defn handler
"Generate declarations for handlers for this `query`. Cache handlers are needed only for select queries."
[query]
(let [handler-name (symbol (:name query))
v (volatility (:entity query))]
(if (and
(number? v)
(> v 0)
(#{:select-1 :select-many :text-search}(:type query)))
(list
'def
handler-name
(str
"Auto-generated function to "
(generate-documentation query))
(list
'memo/ttl
(list
'fn
['connection 'params]
(list
(symbol (str "db/" (:name query)))
'connection 'params))
{}
:ttl/threshold
(* v 1000))))))
(defn to-cache
"Generate a `/cache.clj` file for this `application`."
[application]
(let [queries-map (queries application)
filepath (str *output-path* "src/clj/" (:name (:attrs application)) "/cache.clj")]
(make-parents filepath)
(do-or-warn
(with-open [output (writer filepath)]
(binding [*out* output]
(pprint (file-header application))
(println)
(doall
(map
(fn [k]
(let [k (handler (queries-map k))]
(if k
(do
(pprint k)
(println)))
k))
(sort (keys queries-map)))))))
(if (pos? *verbosity*)
(*warn* (str "\tGenerated " filepath)))))

View file

@ -423,3 +423,74 @@
(if (pos? *verbosity*) (if (pos? *verbosity*)
(*warn* (str "\tGenerated " filepath))))))) (*warn* (str "\tGenerated " filepath)))))))
(defn generate-documentation
"Generate, as a string, appropriate documentation for a function wrapping this `query` map."
[query]
(let [v (volatility (:entity query))]
(s/join
" "
(list
(case
(:type query)
:delete-1
(str "delete one record from the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(-> query :entity key-names)
"`.")
:insert-1
(str "insert one record to the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity insertable-properties )))
"`. Returns a map containing the keys `"
(-> query :entity key-names)
"` identifying the record created.")
:select-1
(str "select one record from the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(-> query :entity key-names)
"`. Returns a map containing the following keys: `"
(map #(keyword (:name (:attrs %))) (-> query :entity all-properties))
"`.")
:select-many
(str "select all records from the `"
(-> query :entity :attrs :name)
"` table. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity all-properties)))
"`.")
:text-search
(str "select all records from the `"
(-> query :entity :attrs :name)
;; TODO: this doc-string is out of date
"` table with any text field matching the value of the key `:pattern` which should be in the request. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity all-properties)))
"`.")
:update-1
(str "update one record in the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(pr-str
(distinct
(sort
(map
#(keyword (:name (:attrs %)))
(flatten
(cons
(-> query :entity key-properties)
(-> query :entity insertable-properties)))))))
"`."))
(if-not
(zero? v)
(str "Results will be held in cache for " v " seconds."))))))

View file

@ -3,7 +3,7 @@
adl.to-json-routes adl.to-json-routes
(:require [adl-support.core :refer :all] (:require [adl-support.core :refer :all]
[adl-support.utils :refer :all] [adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [queries]] [adl.to-hugsql-queries :refer [generate-documentation queries]]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f] [clj-time.format :as f]
[clojure.java.io :refer [file make-parents writer]] [clojure.java.io :refer [file make-parents writer]]
@ -65,6 +65,7 @@
'[noir.response :as nresponse] '[noir.response :as nresponse]
'[noir.util.route :as route] '[noir.util.route :as route]
'[ring.util.http-response :as response] '[ring.util.http-response :as response]
(vector (symbol (str (safe-name (:name (:attrs application))) ".cache")) :as 'cache)
(vector (symbol (str (safe-name (:name (:attrs application))) ".db.core")) :as 'db)))) (vector (symbol (str (safe-name (:name (:attrs application))) ".db.core")) :as 'db))))
@ -80,6 +81,19 @@
[query] [query]
(list (list
['request] ['request]
(let
[v (volatility (:entity query))
function (symbol (str
(if
(and
(number? v)
(> v 0)
(#{:select-1 :select-many :text-search} (:type query)))
"cache"
"db")
"/"
(:name query)))]
(list (list
'let 'let
['params (list ['params (list
@ -101,7 +115,7 @@
(list (list
'do-or-server-fail 'do-or-server-fail
(list (list
(symbol (str "db/" (:name query))) function
'db/*db* 'params) 'db/*db* 'params)
(case (:type query) (case (:type query)
:insert-1 201 ;; created :insert-1 201 ;; created
@ -122,36 +136,26 @@
(-> query :entity key-properties) (-> query :entity key-properties)
;; default ;; default
nil)))) nil))))
'request)))) 'request)))))
(defn generate-handler-src (defn generate-handler-src
"Generate and return the handler for this `query`." "Generate and return the handler for this `query`."
[handler-name query-map method doc] [handler-name query-map method]
(let [doc (str
"Auto-generated function to "
(generate-documentation query-map))
v (volatility (:entity query-map))]
(hash-map (hash-map
:method method :method method
:src (remove :src (remove
nil? nil?
(if
(or
(zero? (volatility (:entity query-map)))
(#{:delete-1 :insert-1 :update-1} (:type query-map)))
(concat (concat
(list (list
'defn 'defn
handler-name handler-name
(str "Auto-generated method to " doc)) doc
(generate-handler-body query-map)) (generate-handler-body query-map)))))))
(concat
(list
'def
handler-name
(list
'memo/ttl
(cons 'fn (generate-handler-body query-map))
{}
:ttl/threshold
(* (volatility (:entity query-map)) 1000))))))))
(defn handler (defn handler
@ -168,76 +172,12 @@
:route (str "/json/" handler-name)} :route (str "/json/" handler-name)}
(case (case
(:type query) (:type query)
:delete-1 (:delete-1 :insert-1 :update-1)
(generate-handler-src (generate-handler-src
handler-name query :post handler-name query :post)
(str "delete one record from the `" (:select-1 :select-many :text-search)
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(-> query :entity key-names)
"`."))
:insert-1
(generate-handler-src (generate-handler-src
handler-name query :post handler-name query :get)
(str "insert one record to the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity insertable-properties )))
"`. Returns a map containing the keys `"
(-> query :entity key-names)
"` identifying the record created."))
:update-1
(generate-handler-src
handler-name query :post
(str "update one record in the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(pr-str
(distinct
(sort
(map
#(keyword (:name (:attrs %)))
(flatten
(cons
(-> query :entity key-properties)
(-> query :entity insertable-properties)))))))
"`."))
:select-1
(generate-handler-src
handler-name query :get
(str "select one record from the `"
(-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `"
(-> query :entity key-names)
"`. Returns a map containing the following keys: `"
(map #(keyword (:name (:attrs %))) (-> query :entity all-properties))
"`."))
:select-many
(generate-handler-src
handler-name query :get
(str "select all records from the `"
(-> query :entity :attrs :name)
"` table. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity all-properties)))
"`."))
:text-search
(generate-handler-src
handler-name query :get
(str "select all records from the `"
(-> query :entity :attrs :name)
;; TODO: this doc-string is out of date
"` table with any text field matching the value of the key `:pattern` which should be in the request. If the keys `(:limit :offset)` are present in the request then they will be used to page through the data. Returns a sequence of maps each containing the following keys: `"
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity all-properties)))
"`."))
(:select-many-to-many (:select-many-to-many
:select-one-to-many) :select-one-to-many)
(hash-map :method :get (hash-map :method :get
@ -249,6 +189,25 @@
(str ";; don't know what to do with query `" :key "` of type `" (:type query) "`."))))))) (str ";; don't know what to do with query `" :key "` of type `" (:type query) "`.")))))))
(defn make-handlers-map
"Analyse this `application` and generate from it a map of the handlers to be output."
[application]
(reduce
merge
{}
(map
(fn [e]
(let [qmap (queries application e)]
(reduce
merge
{}
(map
(fn [k]
(handler k qmap application))
(keys qmap)))))
(children-with-tag application :entity))))
(defn defroutes (defn defroutes
"Generate JSON routes for all queries implied by this ADL `application` spec." "Generate JSON routes for all queries implied by this ADL `application` spec."
[handlers-map] [handlers-map]
@ -269,25 +228,6 @@
(keys handlers-map)))))) (keys handlers-map))))))
(defn make-handlers-map
"Analyse this `application` and generate from it a map of the handlers to be output."
[application]
(reduce
merge
{}
(map
(fn [e]
(let [qmap (queries application e)]
(reduce
merge
{}
(map
(fn [k]
(handler k qmap application))
(keys qmap)))))
(children-with-tag application :entity))))
(defn to-json-routes (defn to-json-routes
"Generate a `/routes/auto-json.clj` file for this `application`." "Generate a `/routes/auto-json.clj` file for this `application`."
[application] [application]
@ -306,8 +246,8 @@
(println) (println)
h) h)
(sort (keys handlers-map)))) (sort (keys handlers-map))))
(pprint (defroutes handlers-map)))) (pprint (defroutes handlers-map)))))
(if (pos? *verbosity*) (if (pos? *verbosity*)
(*warn* (str "\tGenerated " filepath)))))) (*warn* (str "\tGenerated " filepath)))))