Merge tag 'adl-support-0.1.6'

This commit is contained in:
Simon Brooke 2019-05-02 15:43:58 +01:00
commit aa835405cd
8 changed files with 231 additions and 60 deletions

View file

@ -1,4 +1,4 @@
(defproject adl-support "0.1.5" (defproject adl-support "0.1.6"
:description "A small library of functions called by generated ADL code." :description "A small library of functions called by generated ADL code."
:url "https://github.com/simon-brooke/adl-support" :url "https://github.com/simon-brooke/adl-support"
:license {:name "MIT License" :license {:name "MIT License"

View file

@ -4,7 +4,8 @@
adl-support.filters adl-support.filters
(:require [clojure.string :as s] (:require [clojure.string :as s]
[selmer.filters :as f] [selmer.filters :as f]
[selmer.parser :as p])) [selmer.parser :as p]
[selmer.tags :as t]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -64,3 +65,24 @@
(f/add-filter! :email email) (f/add-filter! :email email)
;; (p/render "{{p|telephone}}" {:p "07768 130255"}) ;; (p/render "{{p|telephone}}" {:p "07768 130255"})
(defn contains
[collection value]
(first
(filter
#(= % value)
collection)))
;; (contains '(:a :b :c) :a)
(f/add-filter! :contains contains)
;; (p/render "{{l|contains:\"foo\"}}" {:l ["froboz" "bar"]})
;; (p/render "{% if l|contains:\"foo\" %}I see ya!{% else %}I don't{% endif %}" {:l ["foo" "bar"]})
;; (p/render "{% if l|contains:\"foo\" %}I see ya!{% else %}I don't{% endif %}" {:l ["froboz" "bar"]})
;; (p/render
;; "<option value='{{option.id}}' {% if record.roles|contains:option.id %}selected='selected'{% endif %}>{{option.name}}</option>"
;; {:option {:id 2 :name "Fred"} :record {:roles [2 6]}})

View file

@ -3,11 +3,11 @@
:author "Simon Brooke"} :author "Simon Brooke"}
adl-support.forms-support adl-support.forms-support
(:require [adl-support.core :refer :all] (:require [adl-support.core :refer :all]
[adl-support.utils :refer [descendants-with-tag safe-name singularise]] [adl-support.utils :refer [capitalise descendants-with-tag safe-name singularise]]
[clojure.core.memoize :as memo] [clojure.core.memoize :as memo]
[clojure.data.json :as json] [clojure.data.json :as json]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.string :refer [lower-case]])) [clojure.string :refer [join lower-case]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -92,7 +92,7 @@
"The name to which data for this `auxlist` will be bound in the "The name to which data for this `auxlist` will be bound in the
Selmer params." Selmer params."
[auxlist] [auxlist]
`(safe-name (str "auxlist-" (-> ~auxlist :attrs :property)) :clojure)) `(safe-name (-> ~auxlist :attrs :property) :clojure))
(defmacro all-keys-present? (defmacro all-keys-present?
@ -122,3 +122,21 @@
(map (map
#(hash-map (keyword (-> % :attrs :name)) (-> % :attrs :default)) #(hash-map (keyword (-> % :attrs :name)) (-> % :attrs :default))
(descendants-with-tag entity :property #(-> % :attrs :default))))) (descendants-with-tag entity :property #(-> % :attrs :default)))))
(defn form-title
"Construct an appropriate title for a form having this `form-name`, for an
entity having these `user-distinct-property-keys`, given this `record`."
[record form-name user-distinct-property-keys]
(str
form-name
": "
(join
", "
(remove
nil?
(map
record
user-distinct-property-keys)))))

View file

@ -46,10 +46,10 @@
;; TODO: candidate for moving to adl-support.core ;; TODO: candidate for moving to adl-support.core
[form request] [form request]
`(if-valid-user `(if-valid-user
~form ~form
~request ~request
{:status 403 {:status 403
:body (json/write-str "You must be logged in to do that")})) :body (json/write-str "You must be logged in to do that")}))
(defmacro with-params-or-error (defmacro with-params-or-error
@ -64,9 +64,6 @@
:body (json/write-str (str "The following params are required: " ~required))})) :body (json/write-str (str "The following params are required: " ~required))}))
;; (with-params-or-error (/ 1 0) {:a 1 :b 2} #{:a :b :c})
;; (with-params-or-error "hello" {:a 1 :b 2} #{:a :b })
(defmacro do-or-server-fail (defmacro do-or-server-fail
"Evaluate this `form`; if it succeeds, return an HTTP response with this "Evaluate this `form`; if it succeeds, return an HTTP response with this
status code and the JSON-formatted result as body; if it fails, return an status code and the JSON-formatted result as body; if it fails, return an
@ -78,6 +75,5 @@
{:status ~status {:status ~status
:body (:result r#)} :body (:result r#)}
{:status 500 {:status 500
:body r#}))) :body r#})))

View file

@ -2,7 +2,9 @@
in generated templates." in generated templates."
:author "Simon Brooke"} :author "Simon Brooke"}
adl-support.tags adl-support.tags
(:require [selmer.parser :as p])) (:require [clojure.string :refer [split]]
[selmer.parser :as p]
[selmer.tags :as t]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -32,6 +34,22 @@
success success
failure)) failure))
(defn parse-arg
[arg context]
(cond
(number? arg)
arg
(number? (read-string arg))
(read-string arg)
(= \" (first arg))
(.substring arg 1 (dec (.length arg)))
(and (= \: (first arg)) (> (count arg) 1))
(keyword (subs arg 1))
:else
(get-in context (map keyword (split arg #"\.")))))
(defn add-tags [] (defn add-tags []
"Add custom tags required by ADL-generated code to the parser's tags." "Add custom tags required by ADL-generated code to the parser's tags."
(p/add-tag! :ifmemberof (p/add-tag! :ifmemberof
@ -43,6 +61,24 @@
:else :else
(fn [args context content] (fn [args context content]
"") "")
:endifmemberof)) :endifmemberof)
(p/add-tag! :ifcontains
(fn [[c v] context content]
(let [value (parse-arg v context)
collection (parse-arg c context)]
(if
(some
#(= % value)
collection)
(get-in content [:ifcontains :content])
(get-in content [:else :content]))))
:else
(fn [args context content]
"")
:endifcontains))
(add-tags) (add-tags)

View file

@ -322,22 +322,6 @@
(= (max n1 n2) 1))) (= (max n1 n2) 1)))
(defn link-related-query-name
"link is tricky. If there's exactly than one link between the two
entities, we need to generate the same name from both
ends of the link"
[property nearside farside]
(if (unique-link? nearside farside)
(let [ordered (sort-by #(-> % :attrs :name) (list nearside farside))]
(str "list-"
(safe-name (first ordered) :sql)
"-by-"
(safe-name (nth ordered 1) :sql)))
(str "list-"
(safe-name property :sql) "-by-"
(singularise (safe-name nearside :sql)))))
(defn link-table-name (defn link-table-name
"Canonical name of a link table between entity `e1` and entity `e2`. However, there "Canonical name of a link table between entity `e1` and entity `e2`. However, there
may be different links between the same two tables with different semantics; if may be different links between the same two tables with different semantics; if
@ -358,33 +342,6 @@
"_" (cons "ln" (map #(:name (:attrs %)) (list property e1))))))) "_" (cons "ln" (map #(:name (:attrs %)) (list property e1)))))))
(defn list-related-query-name
"Return the canonical name of the HugSQL query to return all records on
`farside` which match a given record on `nearside`, where `nearide` and
`farside` are both entities."
[property nearside farside]
(if
(and
(property? property)
(entity? nearside)
(entity? farside))
(case (-> property :attrs :type)
"link" (link-related-query-name property nearside farside)
"list" (str "list-"
(safe-name farside :sql) "-by-"
(singularise (safe-name nearside :sql)))
"entity" (str "list-"
(safe-name nearside :sql) "-by-"
(singularise (safe-name farside :sql)))
;; default
(str "ERROR-bad-property-type-"
(-> ~property :attrs :type) "-of-"
(-> ~property :attrs :name)))
(do
(*warn* "Argument passed to `list-related-query-name` was a non-entity")
nil)))
(defn property-for-field (defn property-for-field
"Return the property within this `entity` which matches this `field`." "Return the property within this `entity` which matches this `field`."
[field entity] [field entity]
@ -618,6 +575,45 @@
#(#{"system" "all"} (:distinct (:attrs %))) #(#{"system" "all"} (:distinct (:attrs %)))
(properties entity))) (properties entity)))
(defn list-related-query-name
"Return the canonical name of the HugSQL query to return all records on
`farside` which match a given record on `nearside`, where `nearide` and
`farside` are both entities."
([property nearside farside as-symbol?]
(let [unique? (=
(count
(filter
#(= (-> % :attrs :entity)(-> property :attrs :entity))
(descendants-with-tag nearside :property)))
1)
farname (if unique? (safe-name farside :sql) (safe-name property :sql))
nearname (singularise (safe-name nearside :sql))
n (case (-> property :attrs :type)
;; TODO: I am deeply susicious of this. It's just improbable that
;; the same recipe should work for all three cases.
("link" "list") (str "list-" farname "-by-" nearname)
"entity" (str "list-" farname "-by-" nearname)
;; default
(str "ERROR-bad-property-type-"
(-> ~property :attrs :type) "-of-"
(-> ~property :attrs :name)))]
(if
(and
(property? property)
(entity? nearside)
(entity? farside))
(if
as-symbol?
(symbol (str "db/" n))
n)
(do
(*warn* "Argument passed to `list-related-query-name` was a non-entity")
nil))))
([property nearside farside]
(list-related-query-name property nearside farside false)))
(defn path-part (defn path-part
"Return the URL path part for this `form` of this `entity` within this `application`. "Return the URL path part for this `form` of this `entity` within this `application`.
Note that `form` may be a Clojure XML representation of a `form`, `list` or `page` Note that `form` may be a Clojure XML representation of a `form`, `list` or `page`
@ -658,3 +654,28 @@
0 0
(expt 10 v))) (expt 10 v)))
(catch Exception _ 0))) (catch Exception _ 0)))
(defn order-preserving-set
"The Clojure `set` function does not preserve the order in which elements are
passed to it. This function is like `set`, except
1. It returns a list, not a hashset, and
2. It is order-preserving."
[collection]
(loop [lhs (list (first collection))
rhs (rest collection)]
(cond
(empty? rhs) (reverse lhs)
(some #(= (first rhs) %) lhs) (recur lhs (rest rhs))
true (recur (cons (first rhs) lhs) (rest rhs)))))
(defmacro entity-by-name
"Return the entity with this `entity-name` in this `application`.
TODO: Candidate for move to adl-support.utils."
[entity-name application]
`(child-with-tag
~application
:entity
#(= (:name (:attrs %)) ~entity-name)))

View file

@ -6,7 +6,7 @@
(add-tags) (add-tags)
(deftest if-member-of-tests (deftest if-member-of-tests
(testing "testing the if-member-of tag" (testing "the `ifmemberof` tag"
(let [expected "boo" (let [expected "boo"
actual (if-member-of-permitted nil nil "caramba" "boo")] actual (if-member-of-permitted nil nil "caramba" "boo")]
(is (= expected actual) "Nil args, nil ")) (is (= expected actual) "Nil args, nil "))
@ -48,3 +48,75 @@
(deftest if-contains-tests
(testing "the `ifcontains` tag"
(let [expected "Hello!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 2 :name "Fred"} :record {:roles [2 6]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first contains the value of the second;
values are numbers"))
(let [expected "Goodbye!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 3 :name "Ginny"} :record {:roles [2 6]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first does not contain the value of the second;
values are numbers"))
(let [expected "Hello!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id :two :name "Fred"} :record {:roles [:two :six]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first contains the value of the second;
values are keywords"))
(let [expected "Goodbye!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id :three :name "Ginny"} :record {:roles [:two :six]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first does not contain the value of the second;
values are keywords"))
(let [expected "Hello!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id "two" :name "Fred"} :record {:roles ["two" "six"]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first contains the value of the second;
values are strings"))
(let [expected "Goodbye!"
actual (parser/render "{% ifcontains record.roles option.id %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id "three" :name "Ginny"} :record {:roles ["two" "six"]}})]
(is (= expected actual)
"Both args are paths which exist in the context;
the value of the first does not contain the value of the second;
values are strings"))
(let [expected "Hello!"
actual (parser/render "{% ifcontains record.roles 2 %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 4 :name "Henry"} :record {:roles [2 6]}})]
(is (= expected actual)
"First arg is a path which exists in the context, second is a literal number;
the value of the first contains the value of the second;
values are numbers"))
(let [expected "Goodbye!"
actual (parser/render "{% ifcontains record.roles 3 %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 3 :name "Ginny"} :record {:roles [2 6]}})]
(is (= expected actual)
"First arg is a path which exists in the context, second is a literal number;
the value of the first does not contain the value of the second;
values are numbers"))
(let [expected "Hello!"
actual (parser/render "{% ifcontains record.roles :two %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 4 :name "Henry"} :record {:roles [:two :six]}})]
(is (= expected actual)
"First arg is a path which exists in the context, second is a literal keyword;
the value of the first contains the value of the second;
values are numbers"))
(let [expected "Goodbye!"
actual (parser/render "{% ifcontains record.roles :three %}Hello!{% else %}Goodbye!{% endifcontains %}"
{:option {:id 3 :name "Ginny"} :record {:roles [:two :six]}})]
(is (= expected actual)
"First arg is a path which exists in the context, second is a literal keyword;
the value of the first does not contain the value of the second;
values are numbers"))))

View file

@ -598,3 +598,9 @@
(is (= (key-names e2) #{"id" "shard"})) (is (= (key-names e2) #{"id" "shard"}))
(is (= (key-names e2 true) #{:id :shard}))))) (is (= (key-names e2 true) #{:id :shard})))))
(deftest order-preserving-set-tests
(testing "order-preserving-set"
(is (= '(:a) (order-preserving-set '(:a :a :a :a))))
(is (= '(:a) (order-preserving-set [:a :a :a :a])))
(is (= '(:a :b :c :d :e) (order-preserving-set '(:a :a :b :c :a :b :d :c :e))))))