Lots of work on trying to get all validator tests to pass.

Three still fail, but substantial progress. The to-husql-queries
tests are failing much worse, but are not a current target (nor is
the validator, frankly, but it irks me that it is so broken)
This commit is contained in:
Simon Brooke 2025-05-22 11:43:48 +01:00
parent 5af9a7349c
commit b944aa6bf1
6 changed files with 677 additions and 639 deletions

View file

@ -5,14 +5,14 @@
:license {:name "GNU Lesser General Public License, version 3.0 or (at your option) any later version"
:url "https://www.gnu.org/licenses/lgpl-3.0.en.html"}
:dependencies [[adl-support "0.1.6"]
:dependencies [[adl-support "0.1.8-SNAPSHOT"]
[bouncer "1.0.1"]
[clojure-saxon "0.9.4"]
[environ "1.1.0"]
[environ "1.2.0"]
[hiccup "1.0.5"]
[org.clojure/clojure "1.8.0"]
[org.clojure/math.combinatorics "0.1.4"]
[org.clojure/tools.cli "0.3.7"]]
[org.clojure/clojure "1.12.0"]
[org.clojure/math.combinatorics "0.3.0"]
[org.clojure/tools.cli "1.1.230"]]
:aot [adl.main]

View file

@ -3,12 +3,10 @@
adl.to-hugsql-queries
(:require [adl-support.core :refer :all]
[adl-support.utils :refer :all]
[clojure.java.io :refer [file make-parents]]
[clojure.java.io :refer [make-parents]]
[clojure.math.combinatorics :refer [combinations]]
[clojure.string :as s]
[clojure.xml :as x]
[clj-time.core :as t]
[clj-time.format :as f]))
[clj-time.core :as t]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -33,10 +31,10 @@
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def expanded-token "_expanded")
(def expanded-token "_expanded")
(defn where-clause
(defn where-clause
"Generate an appropriate `where` clause for queries on this `entity`;
if `properties` are passed, filter on those properties, otherwise the key
properties."
@ -46,7 +44,7 @@
(let
[entity-name (safe-name entity :sql)
property-names (map #(:name (:attrs %)) properties)]
(if-not (empty? property-names)
(when-not (empty? property-names)
(str
"WHERE "
(s/join
@ -56,7 +54,7 @@
property-names)))))))
(defn order-by-clause
(defn order-by-clause
"Generate an appropriate `order by` clause for queries on this `entity`"
([entity]
(order-by-clause entity "" false))
@ -89,7 +87,7 @@
;; (order-by-clause e "" true)
(defn insert-query
(defn insert-query
"Generate an appropriate `insert` query for this `entity`.
TODO: this depends on the idea that system-unique properties
are not insertable, which is... dodgy."
@ -128,7 +126,7 @@
(key-names entity))))))})))
(defn update-query
(defn update-query
"Generate an appropriate `update` query for this `entity`"
[entity]
(let [entity-name (safe-name entity :sql)
@ -158,13 +156,19 @@
(where-clause entity))})))
(defn search-query [entity application]
"Generate an appropriate search query for string fields of this `entity`"
(defn search-query
"Generate an appropriate search query for string fields of this `entity`.
Unused second argument was `application`, and is retained for backward
compatibility."
([entity _]
(search-query entity))
([entity]
(let [entity-name (safe-name entity :sql)
pretty-name (singularise entity-name)
query-name (str "search-strings-" entity-name)
signature ":? :*"
properties (remove #(#{"(safe-name entity :sql)"}(:type (:attrs %))) (all-properties entity))]
properties (remove #(#{"(safe-name entity :sql)"} (:type (:attrs %))) (all-properties entity))]
(hash-map
(keyword query-name)
{:name query-name
@ -215,11 +219,10 @@
properties))))
(order-by-clause entity "lv_" true)
"--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))}))))
;; (search-query e a)
(defn select-query
"Generate an appropriate `select` query for this `entity`"
([entity properties]
@ -229,7 +232,9 @@
pretty-name (singularise entity-name)
query-name (if (= properties (key-properties entity))
(str "get-" pretty-name)
(str "get-" pretty-name "-by-" (s/join "=" (map #(:name (:attrs %)) properties))))
(str "get-" pretty-name "-by-"
(s/join "="
(map #(:name (:attrs %)) properties))))
signature ":? :1"]
(hash-map
(keyword query-name)
@ -350,8 +355,7 @@
(str "SELECT DISTINCT lv_" safe-far ".* \nFROM lv_" safe-far)
(str "WHERE lv_" safe-far "." (safe-name (first (key-names far-entity)) :sql) " = :id")
(order-by-clause far-entity "lv_" false))
(list (str "ERROR: unexpected type " link-type " of property " %)))))
}))
(list (str "ERROR: unexpected type " link-type " of property " %)))))}))
links))))
@ -446,7 +450,7 @@
(pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity insertable-properties )))
(-> query :entity insertable-properties)))
"`. Returns a map containing the keys `"
(-> query :entity key-names)
"` identifying the record created.")

View file

@ -3,10 +3,9 @@
adl.to-psql
(:require [adl-support.core :refer :all]
[adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [queries]]
[clojure.java.io :refer [file make-parents writer]]
;; [adl.to-hugsql-queries :refer [queries]]
[clojure.java.io :refer [make-parents]]
[clojure.string :as s]
[clojure.xml :as x]
[clj-time.core :as t]
[clj-time.format :as f]))
@ -220,7 +219,7 @@
(if
key?
"NOT NULL PRIMARY KEY"
(if (= (:required (:attrs property)) "true") "NOT NULL"))))))))))
(when (= (:required (:attrs property)) "true") "NOT NULL"))))))))))
(defn compose-convenience-entity-field
@ -259,6 +258,7 @@
(all-properties entity)
(user-distinct-properties entity)))))))
(declare compose-convenience-where-clause)
(defn compose-convenience-where-clause
"Compose an SQL `WHERE` clause for a convenience view of this

View file

@ -2,11 +2,11 @@
TODO: this is at present largely a failed experiment."
:author "Simon Brooke"}
adl.validator
(:require [adl-support.utils :refer :all]
(:require [adl-support.utils :refer []]
[clojure.set :refer [union]]
[clojure.xml :refer [parse]]
[bouncer.core :as b]
[bouncer.validators :as v]))
[bouncer.validators :as v :refer [every member required string]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
@ -43,11 +43,11 @@
(symbol? validation)
(try
(b/validate o validation)
(catch java.lang.ClassCastException c
(catch java.lang.ClassCastException _
;; The validator regularly barfs on strings, which are perfectly
;; valid content of some elements. I need a way to validate
;; elements where they're not tolerated!
(if (string? o) [nil o]))
(when (string? o) [nil o]))
(catch Exception e
[{:error (.getName (.getClass e))
:message (.getMessage e)
@ -55,27 +55,26 @@
:context o} o]))
[(str "Error: not a symbol" validation) o]))
(defmacro disjunct-valid?
(defn disjunct-valid?
"Yes, this is a horrible hack. I should be returning the error structure
not printing it. But I can't see how to make that work with `bouncer`.
OK, so: most of the validators will (usually) fail, and that's OK. How
do we identify the one which ought not to have failed?"
[o & validations]
`(println
(println
(str
(if (:tag ~o) (str "Tag: " (:tag ~o) "; "))
(if (:name (:attrs ~o)) (str "Name: " (:name (:attrs ~o)) ";"))
(if-not (or (:tag ~o) (:name (:attrs ~o))) (str "Context: " ~o))))
(when (:tag o) (str "Tag: " (:tag o) "; "))
(when (:name (:attrs o)) (str "Name: " (:name (:attrs o)) ";"))
(when-not (or (:tag o) (:name (:attrs o))) (str "Context: " o))))
`(empty?
(empty?
(remove :tag (remove nil? (map first (map
#(try-validate ~o '%)
~validations))))))
#(try-validate o '%)
validations))))))
;;; the remainder of this file is a fairly straight translation of the ADL 1.4 DTD into Clojure
(declare documentation-validations fieldgroup-validations )
(declare documentation-validations fieldgroup-validations)
(def permissions
"permissions a group may have on an entity, list, page, form or field
@ -170,7 +169,7 @@
(def sequences #{"canonical", "reverse-canonical"})
(def reference-validations
"The 'specification' and 'reference' elements are for documentation only,
"The 'specification' and 'reference' elements are for documentation only,
and do not contribute to the engineering of the application described.
A reference element is a reference to a specifying document.
@ -191,6 +190,15 @@
[:attrs :property] v/string ;; and should be the name of a property in that entity
:content [[v/every documentation-validations]]})
;; (def sample-reference {:tag :reference
;; :attrs {:abbr "foo"
;; :section "bar"
;; :entity "animal"
;; :property "breed"}
;; :content [{:tag :documentation
;; :content ["Every animal should have a breed."]}]})
;; (b/validate sample-reference reference-validations)
(def specification-validations
"The 'specification' and 'reference' elements are for documentation only,
@ -208,20 +216,27 @@
[:attrs :name] [v/string v/required]
[:attrs :abbr] [v/string v/required]
:content [[v/every #(disjunct-valid?
%
documentation-validations
reference-validations)]]})
(def documentation-validations
"contains documentation on the element which immediately contains it. TODO:
should HTML markup within a documentation element be allowed? If so, are
there restrictions?"
"contains documentation on the element which immediately contains it. For
the time being, HTML markup is not permitted within documentation, but
Markdown (which may include a string representation of HTML markup) should
be."
{:tag [v/required [#(= % :documentation)]]
:content [[v/every #(disjunct-valid?
%
v/string
reference-validations)]]
})
reference-validations)]]})
;; (def sample-documentation {:tag :documentation
;; :content ["Every animal should have a breed."
;; sample-reference]})
;; (b/validate sample-documentation documentation-validations)
;; (b/valid? sample-documentation documentation-validations)
(def content-validations
{:tag [v/required [#(= % :content)]]})
@ -293,6 +308,17 @@
(b/valid? % documentation-validations)
(b/valid? % prompt-validations))]]})
(def sample-option {:tag :option,
:attrs {:value "Female"},
:content
[{:tag :prompt,
:attrs {:locale "fr-FR", :prompt "Femme"},
:content nil}
{:tag :prompt,
:attrs {:locale "en-GB", :prompt "Female"},
:content nil}]})
(b/validate sample-option option-validations)
(def pragma-validations
"pragmatic advice to generators of lists and forms, in the form of
name/value pairs which may contain anything. Over time some pragmas
@ -302,8 +328,6 @@
[:attrs :name] [v/string v/required]
[:attrs :value] [v/string v/required]})
(def generator-validations
"marks a property which is auto-generated by some part of the system.
This is based on the Hibernate construct, except that the Hibernate
@ -425,34 +449,54 @@
committed to persistent store, the value which it holds before
it has been committed"
{:tag [v/required [#(= % :property)]]
[:attrs :name] [v/required v/string]
[:attrs :type] [v/required [v/member all-data-types]]
;; [:attrs :default] [] ;; it's allowed, but I don't have anything particular to say about it
[:attrs :typedef] v/string
[:attrs :distinct] [v/string [v/member #{"none", "all", "user", "system"}]]
[:attrs :entity] v/string
[:attrs :farkey] v/string
[:attrs :required] [[v/member #{"true", "false"}]]
[:attrs :immutable] [[v/member #{"true", "false"}]]
[:attrs :name] [v/required v/string]
[:attrs :required] [[v/member #{"true", "false"}]]
[:attrs :size] [[#(cond
(empty? %) ;; it's allowed to be missing
true
(string? %)
(integer? (read-string %))
true
:else
(integer? %))]]
[:attrs :type] [v/required [v/member all-data-types]]
;; [:attrs :default] [] ;; it's allowed, but I don't have anything particular to say about it
[:attrs :typedef] v/string
[:attrs :cascade] [[v/member cascade-actions]]
[:attrs :column] v/string
[:attrs :concrete] [[v/member #{"true", "false"}]]
[:attrs :cascade] [[v/member cascade-actions]]
;; :content [[v/every #(disjunct-valid? %
;; documentation-validations
:content [[v/every #(disjunct-valid? %
documentation-validations
generator-validations
permission-validations
option-validations
prompt-validations
help-validations
ifmissing-validations)]]
})
;; (disjunct-valid? sample-option documentation-validations
;; generator-validations
;; permission-validations
;; option-validations
;; prompt-validations
;; help-validations
;; ifmissing-validations)]]
})
;; ifmissing-validations)
;; (def sample-property {:tag :property,
;; :attrs
;; {:immutable "true",
;; :required "true",
;; :distinct "system",
;; :type "integer",
;; :name "id"},
;; :content
;; [{:tag :generator, :attrs {:action "native"}, :content nil}]})
;; (b/validate sample-property property-validations)
(def permission-validations

View file

@ -1,8 +1,8 @@
(ns adl.to-hugsql-queries-test
(:require [clojure.string :as s]
[clojure.test :refer :all]
[adl.to-hugsql-queries :refer :all]
[adl-support.utils :refer :all]))
[clojure.test :refer [deftest is testing]]
[adl.to-hugsql-queries :refer [delete-query insert-query list-query order-by-clause search-query select-query update-query]]
[adl-support.utils :refer [child-with-tag has-non-key-properties? has-primary-key? key-names]]))
(defn string-equal-ignore-whitespace?
"I don't want unit tests to fail just because emitted whitespace changes."
@ -47,16 +47,14 @@
:size "12"
:name "postcode"},
:content
[{:tag :generator, :attrs {:action "native"}, :content nil}]}
]}
[{:tag :generator, :attrs {:action "native"}, :content nil}]}]}
{:tag :property,
:attrs
{:distinct "user", :size "128", :type "string", :name "street"},
:content nil}
{:tag :property,
:attrs {:size "64", :type "string", :name "town"},
:content nil}
]}]}
:content nil}]}]}
entity (child-with-tag application :entity)]
(testing "user distinct properties should provide the default ordering"
(let [expected
@ -64,7 +62,6 @@
actual (order-by-clause entity)]
(is (string-equal-ignore-whitespace? actual expected))))))
(deftest keys-name-extraction-tests
(let [application {:tag :application,
:attrs {:version "0.1.1", :name "test-app"},
@ -94,16 +91,14 @@
:size "12"
:name "postcode"},
:content
[{:tag :generator, :attrs {:action "native"}, :content nil}]}
]}
[{:tag :generator, :attrs {:action "native"}, :content nil}]}]}
{:tag :property,
:attrs
{:distinct "user", :size "128", :type "string", :name "street"},
:content nil}
{:tag :property,
:attrs {:size "64", :type "string", :name "town"},
:content nil}
]}]}
:content nil}]}]}
entity (child-with-tag application :entity)]
(testing "keys name extraction"
(let [expected #{"id" "postcode"}
@ -140,16 +135,14 @@
:size "12"
:name "postcode"},
:content
[{:tag :generator, :attrs {:action "native"}, :content nil}]}
]}
[{:tag :generator, :attrs {:action "native"}, :content nil}]}]}
{:tag :property,
:attrs
{:distinct "user", :size "128", :type "string", :name "street"},
:content nil}
{:tag :property,
:attrs {:size "64", :type "string", :name "town"},
:content nil}
]}]}
:content nil}]}]}
entity (child-with-tag application :entity)]
(testing "keys name extraction"
(let [expected #{"id"}
@ -248,9 +241,7 @@
(testing "delete query signature"
(let [expected ":! :n"
actual (:signature (first (vals (delete-query entity))))]
(is (string-equal-ignore-whitespace? actual expected))))
))
(is (string-equal-ignore-whitespace? actual expected))))))
(deftest complex-key-tests
(let [application {:tag :application,
@ -281,16 +272,14 @@
:size "12"
:name "postcode"},
:content
[{:tag :generator, :attrs {:action "native"}, :content nil}]}
]}
[{:tag :generator, :attrs {:action "native"}, :content nil}]}]}
{:tag :property,
:attrs
{:distinct "user", :size "128", :type "string", :name "street"},
:content nil}
{:tag :property,
:attrs {:size "64", :type "string", :name "town"},
:content nil}
]}]}
:content nil}]}]}
entity (child-with-tag application :entity)]
(testing "user distinct properties should provide the default ordering"
(let [expected "ORDER BY address.street,

View file

@ -1,9 +1,10 @@
(ns adl.validator-test
(:require [clojure.java.io :refer [writer]]
[clojure.test :refer :all]
[clojure.xml :refer [parse]]
(:require
[adl.validator :refer :all]
[bouncer.core :refer [valid?]]))
[bouncer.core :refer [valid? validate]]
[clojure.java.io :refer [writer]]
[clojure.test :refer :all]
[clojure.xml :refer [parse]]))
;; OK, so where we're up to: documentation breaks validation of the
;; element that contains it if the documentation is non-empty.
@ -262,9 +263,9 @@
:name "id"},
:content
[{:tag :generator, :attrs {:action "native"}, :content nil}]}]}
expected true
actual (binding [*out* (writer "/dev/null")]
(valid? xml key-validations))]
expected nil
actual (first (binding [*out* (writer "/dev/null")]
(validate xml key-validations)))]
(is (= actual expected)))))
(deftest validator-property