Merge mess...

This commit is contained in:
Simon Brooke 2018-06-30 20:17:53 +01:00
commit 48c8b6445b
13 changed files with 848 additions and 1134 deletions

View file

@ -2,6 +2,21 @@
A language for describing applications, from which code can be automatically generated. A language for describing applications, from which code can be automatically generated.
## Usage
A document describing the proposed application should be written in XML using the DTD `resources/schemas/adl-1.4.1.dtd`. It may then be transformed into a C# or Java application using the XSL transforms, see **History** below, but this code is very out of date and the resulting application is unlikely to be very usable. Alternatively, it can be transformed into a Clojure [Luminus](http://www.luminusweb.net/) application using the Clojure transformation, as follows:
simon@fletcher:~/workspace/adl$ java -jar target/adl-1.4.1-SNAPSHOT-standalone.jar --help
Usage: java -jar adl-[VERSION]-SNAPSHOT-standalone.jar -options [adl-file]
where options include:
-a, --abstract-key-name-convention [string]: the abstract key name convention to use for generated key fields (TODO: not yet implemented); (default: id)
-h, --help: Show this message
-l, --locale [LOCALE]: set the locale to generate; (default: en_GB.UTF-8)
-p, --path [PATH]: The path under which generated files should be written; (default: generated)
-v, --verbosity [LEVEL], : Verbosity level - integer value required; (default: 0)
This is not yet complete but it is at an advanced stage and already produces code which is useful.
## History ## History
This idea started back in 2007, when I felt that web development in Java had really reached the end of the road - one spent all one's time writing boilerplate, and the amount of time taken to achieve anything useful had expanded far beyond common sense. So I thought: write one high level document describing an application; write a series of transforms from that document to the different files required to build the application; and a great deal of time would be saved. This idea started back in 2007, when I felt that web development in Java had really reached the end of the road - one spent all one's time writing boilerplate, and the amount of time taken to achieve anything useful had expanded far beyond common sense. So I thought: write one high level document describing an application; write a series of transforms from that document to the different files required to build the application; and a great deal of time would be saved.
@ -26,6 +41,10 @@ The idea is that the ADL framework should autogenerate 95% of your application.
A Document Type Definition is the core of this; the current version is `adl-1.4.dtd`. A Document Type Definition is the core of this; the current version is `adl-1.4.dtd`.
### The Clojure transformer application
This is the future direction of the project. Currently it converts a valid ADL XML document into most of the files required for a Clojure web-app. Shortly it will produce a complete Clojure [Luminus](http://www.luminusweb.net/) web-app. In future it may produce web-apps in other languages and frameworks.
### XSL transforms ### XSL transforms
XSL transforms exist which transform conforming documents as follows: XSL transforms exist which transform conforming documents as follows:
@ -54,6 +73,6 @@ I will happily accept pull requests for new XSL transforms (although I'd like so
## License ## License
Copyright © Simon Brooke 2007-2018 Copyright © Simon Brooke 2007-2018; some work was done under contract to [Cygnet Solutions Ltd](http://cygnets.co.uk/), but they have kindly transferred the copyright back to me.
Distributed under the Gnu GPL version 2 or any later version; I am open to licensing this project under additional licences if required. Distributed under the Gnu GPL version 2 or any later version; I am open to licensing this project under additional licences if required.

View file

@ -3,9 +3,12 @@
:url "http://example.com/FIXME" :url "http://example.com/FIXME"
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version" :license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"} :url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[adl-support "0.1.0-SNAPSHOT"]
[org.clojure/clojure "1.8.0"]
[org.clojure/math.combinatorics "0.1.4"] [org.clojure/math.combinatorics "0.1.4"]
[org.clojure/tools.cli "0.3.7"]
[bouncer "1.0.1"] [bouncer "1.0.1"]
[environ "1.1.0"]
[hiccup "1.0.5"]] [hiccup "1.0.5"]]
:aot [adl.main] :aot [adl.main]
:main adl.main :main adl.main

View file

@ -246,7 +246,7 @@ that we can allow HTML block level entities within content elements -->
an entity which has properties and relationships; maps onto a database an entity which has properties and relationships; maps onto a database
table or a Java serialisable class - or, of course, various other things table or a Java serialisable class - or, of course, various other things
name: obviously, the name of this entity name: obviously, the name of this entity.
natural-key: if present, the name of a property of this entity which forms natural-key: if present, the name of a property of this entity which forms
a natural primary key [NOTE: Only partly implemented. NOTE: much of a natural primary key [NOTE: Only partly implemented. NOTE: much of
the present implementation assumes all primary keys will be the present implementation assumes all primary keys will be
@ -254,12 +254,17 @@ that we can allow HTML block level entities within content elements -->
'key' element, below. 'key' element, below.
table: the name of the table in which this entity is stored. Defaults to same table: the name of the table in which this entity is stored. Defaults to same
as name of entity. Strongly recommend this is not used unless it needs as name of entity. Strongly recommend this is not used unless it needs
to be different from the name of the entity to be different from the name of the entity.
foreign: this entity is part of some other system; no code will be generated foreign: this entity is part of some other system; no code will be generated
for it, although code which links to it will be generated for it, although code which links to it will be generated.
magnitude: The power of ten which approximates the expected number of records; thus magnitude: The power of ten which approximates the expected number of records; thus
if ten records are expected, the magnitude is 1; if a million, the if ten records are expected, the magnitude is 1; if a million, the
magnitude is 6 magnitude is 6.
volatility: Number representing the anticipated rate of change of records in this
entity; if 0, results should never be cached; otherwise, a power of
10 representing the number of seconds the data may safely be cached.
thus 5 represents a cach time to live of 100,000 seconds, or slightly
more than one day.
--> -->
<!ELEMENT entity ( documentation?, prompt*, content?, key?, <!ELEMENT entity ( documentation?, prompt*, content?, key?,
property*, permission*, (form | page | list)*)> property*, permission*, (form | page | list)*)>
@ -268,7 +273,8 @@ that we can allow HTML block level entities within content elements -->
natural-key CDATA #IMPLIED natural-key CDATA #IMPLIED
table CDATA #IMPLIED table CDATA #IMPLIED
foreign %Boolean; #IMPLIED foreign %Boolean; #IMPLIED
magnitude CDATA #IMPLIED> magnitude CDATA #IMPLIED
volatility CDATA #IMPLIED>
<!-- <!--
contains documentation on the element which immediately contains it. TODO: contains documentation on the element which immediately contains it. TODO:

View file

@ -90,6 +90,9 @@
<xsl:if test="not(@magnitude)"> <xsl:if test="not(@magnitude)">
<xsl:attribute name="magnitude">6</xsl:attribute> <xsl:attribute name="magnitude">6</xsl:attribute>
</xsl:if> </xsl:if>
<xsl:if test="not(@volatility)">
<xsl:attribute name="volatility">0</xsl:attribute>
</xsl:if>
<xsl:if test="not( @table)"> <xsl:if test="not( @table)">
<xsl:attribute name="table"> <xsl:attribute name="table">
<xsl:value-of select="concat( $tablename-prefix, @name)"/> <xsl:value-of select="concat( $tablename-prefix, @name)"/>

View file

@ -1,13 +1,17 @@
(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.utils :refer :all] (:require [adl-support.utils :refer :all]
[adl.to-hugsql-queries :as h] [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]
[adl.to-selmer-templates :as t] [adl.to-selmer-templates :as t]
[clojure.xml :as x]) [clojure.java.io :refer [make-parents]]
[clojure.string :refer [join]]
[clojure.tools.cli :refer [parse-opts]]
[clojure.xml :as x]
[environ.core :refer [env]])
(:gen-class)) (:gen-class))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -33,22 +37,96 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn print-usage [_] (def cli-options
(println "Argument should be a pathname to an ADL file")) [["-a" "--abstract-key-name-convention [string]" "the abstract key name convention to use for generated key fields (TODO: not yet implemented)"
:default "id"]
["-h" "--help" "Show this message"
:default false]
["-l" "--locale [LOCALE]" "set the locale to generate"
:default (env :lang)]
["-p" "--path [PATH]" "The path under which generated files should be written"
:default "generated"]
["-v" "--verbosity [LEVEL]" nil "Verbosity level - integer value required"
:parse-fn #(Integer/parseInt %)
:default 0]
])
(defn- doc-part
"An `option` in cli-options comprises a sequence of strings followed by
keyword/value pairs. Return all the strings before the first keyword."
[option]
(if
(keyword? (first option)) nil
(cons (first option) (doc-part (rest option)))))
(defn map-part
"An `option` in cli-options comprises a sequence of strings followed by
keyword/value pairs. Return the keyword/value pairs as a map."
[option]
(cond
(empty? option) nil
(keyword? (first option)) (apply hash-map option)
true
(map-part (rest option))))
(defn print-usage []
(println
(join
"\n"
(flatten
(list
(join
(list
"Usage: java -jar adl-"
(or (System/getProperty "adl.version") "[VERSION]")
"-SNAPSHOT-standalone.jar -options [adl-file]"))
"where options include:"
(map
#(let
[doc-part (doc-part %)
default (:default (map-part %))
default-string (if default (str "; (default: " default ")"))]
(str "\t" (join ", " (butlast doc-part)) ": " (last doc-part) default-string))
cli-options))))))
(defn -main (defn -main
"Expects as arg the path-name of an ADL file." "Expects as arg the path-name of an ADL file."
[& args] [& args]
(let [options (parse-opts args cli-options)]
(cond (cond
(empty? args) (empty? args)
(print-usage args) (print-usage)
(.exists (java.io.File. (first args))) (not (empty? (:errors options)))
(let [application (x/parse (first args))] (do
(doall
(map
println
(:errors options)))
(print-usage))
(-> options :options :help)
(print-usage)
true
(do
(let [p (:path (:options options))
op (if (.endsWith p "/") p (str p "/"))]
(binding [*output-path* op
*locale* (-> options :options :locale)
*verbosity* (-> options :options :verbosity)]
(make-parents *output-path*)
(doall
(map
#(if
(.exists (java.io.File. %))
(let [application (x/parse %)]
(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)
(s/to-selmer-routes application) (s/to-selmer-routes application)
(t/to-selmer-templates application)))) (t/to-selmer-templates application))
(println (str "ERROR: File not found: " %)))
(-> options :arguments)))))))))

View file

@ -7,7 +7,7 @@
[clojure.xml :as x] [clojure.xml :as x]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f] [clj-time.format :as f]
[adl.utils :refer :all])) [adl-support.utils :refer :all]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -58,9 +58,9 @@
(order-by-clause entity "")) (order-by-clause entity ""))
([entity prefix] ([entity prefix]
(let (let
[entity-name (:name (:attrs entity)) [entity-name (safe-name (:name (:attrs entity)) :sql)
preferred (map preferred (map
#(:name (:attrs %)) #(safe-name (:name (:attrs %)) :sql)
(filter #(#{"user" "all"} (-> % :attrs :distinct)) (filter #(#{"user" "all"} (-> % :attrs :distinct))
(children entity #(= (:tag %) :property))))] (children entity #(= (:tag %) :property))))]
(if (if
@ -70,7 +70,9 @@
"ORDER BY " prefix entity-name "." "ORDER BY " prefix entity-name "."
(s/join (s/join
(str ",\n\t" prefix entity-name ".") (str ",\n\t" prefix entity-name ".")
(flatten (cons preferred (key-names entity))))))))) (map
#(safe-name % :sql)
(flatten (cons preferred (key-names entity))))))))))
(defn insert-query (defn insert-query
@ -78,9 +80,11 @@
TODO: this depends on the idea that system-unique properties TODO: this depends on the idea that system-unique properties
are not insertable, which is... dodgy." are not insertable, which is... dodgy."
[entity] [entity]
(let [entity-name (:name (:attrs entity)) (let [entity-name (safe-name (:name (:attrs entity)) :sql)
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
insertable-property-names (map #(:name (:attrs %)) (insertable-properties entity)) insertable-property-names (map
#(safe-name (:name (:attrs %)) :sql)
(insertable-properties entity))
query-name (str "create-" pretty-name "!") query-name (str "create-" pretty-name "!")
signature ":! :n"] signature ":! :n"]
(hash-map (hash-map
@ -99,7 +103,12 @@
")" ")"
(if (if
(has-primary-key? entity) (has-primary-key? entity)
(str "\nreturning " (s/join ",\n\t" (key-names entity)))))}))) (str "\nreturning "
(s/join
",\n\t"
(map
#(safe-name % :sql)
(key-names entity))))))})))
(defn update-query (defn update-query
@ -109,7 +118,7 @@
(and (and
(has-primary-key? entity) (has-primary-key? entity)
(has-non-key-properties? entity)) (has-non-key-properties? entity))
(let [entity-name (:name (:attrs entity)) (let [entity-name (safe-name (:name (:attrs entity)) :sql)
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
property-names (map #(:name (:attrs %)) (insertable-properties entity)) property-names (map #(:name (:attrs %)) (insertable-properties entity))
query-name (str "update-" pretty-name "!") query-name (str "update-" pretty-name "!")
@ -125,15 +134,15 @@
"-- :doc updates an existing " pretty-name " record\n" "-- :doc updates an existing " pretty-name " record\n"
"UPDATE " entity-name "\n" "UPDATE " entity-name "\n"
"SET " "SET "
(s/join ",\n\t" (map #(str % " = " (keyword %)) property-names)) (s/join ",\n\t" (map #(str (safe-name % :sql) " = " (keyword %)) property-names))
"\n" "\n"
(where-clause entity))})) (where-clause entity))}))
{})) {}))
(defn search-query [entity] (defn search-query [entity application]
"Generate an appropriate search query for string fields of this `entity`" "Generate an appropriate search query for string fields of this `entity`"
(let [entity-name (:name (:attrs entity)) (let [entity-name (safe-name (:name (:attrs entity)) :sql)
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
query-name (str "search-strings-" pretty-name) query-name (str "search-strings-" pretty-name)
signature ":? :1" signature ":? :1"
@ -155,17 +164,31 @@
"-- :doc selects existing " "-- :doc selects existing "
pretty-name pretty-name
" records having any string field matching the parameter of the same name by substring match") " records having any string field matching the parameter of the same name by substring match")
(str "SELECT * FROM lv_" entity-name) (str "SELECT DISTINCT * FROM lv_" entity-name)
"WHERE "
(s/join (s/join
"\n\tOR " "\n\t--~ "
(cons
"WHERE false"
(filter (filter
string? string?
(map (map
#(if #(str
(#{"string" "date" "text"} (:type (:attrs %))) "(if (:" (-> % :attrs :name) " params) \"OR "
(str (-> % :attrs :name) " LIKE '%params." (-> % :attrs :name) "%'")) (case (base-type % application)
properties))) ("string" "text")
(str
(safe-name (-> % :attrs :name) :sql)
" LIKE '%:" (-> % :attrs :name) "%'")
("date" "time" "timestamp")
(str
(safe-name (-> % :attrs :name) :sql)
" = ':" (-> % :attrs :name) "'")
(str
(safe-name (-> % :attrs :name) :sql)
" = :"
(-> % :attrs :name)))
"\")")
properties))))
(order-by-clause entity "lv_") (order-by-clause entity "lv_")
"--~ (if (:offset params) \"OFFSET :offset \")" "--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))}))) "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))
@ -176,7 +199,7 @@
([entity properties] ([entity properties]
(if (if
(not (empty? properties)) (not (empty? properties))
(let [entity-name (:name (:attrs entity)) (let [entity-name (safe-name (:name (:attrs entity)) :sql)
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
query-name (if (= properties (key-properties entity)) query-name (if (= properties (key-properties entity))
(str "get-" pretty-name) (str "get-" pretty-name)
@ -216,7 +239,7 @@
Parameters `:limit` and `:offset` may be supplied. If not present limit defaults Parameters `:limit` and `:offset` may be supplied. If not present limit defaults
to 100 and offset to 0." to 100 and offset to 0."
[entity] [entity]
(let [entity-name (:name (:attrs entity)) (let [entity-name (safe-name (:name (:attrs entity)) :sql)
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
query-name (str "list-" entity-name) query-name (str "list-" entity-name)
signature ":? :*"] signature ":? :*"]
@ -234,7 +257,7 @@
(list (list
(str "-- :name " query-name " " signature) (str "-- :name " query-name " " signature)
(str "-- :doc lists all existing " pretty-name " records") (str "-- :doc lists all existing " pretty-name " records")
(str "SELECT * FROM lv_" entity-name) (str "SELECT DISTINCT * FROM lv_" entity-name)
(order-by-clause entity "lv_") (order-by-clause entity "lv_")
"--~ (if (:offset params) \"OFFSET :offset \")" "--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))}))) "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))
@ -298,80 +321,6 @@
})) }))
links)))) links))))
(defn link-table-query
"Generate a query which links across the entity passed as `link`
from the entity passed as `near` to the entity passed as `far`.
TODO: not working?"
[near link far]
(if
(and
(entity? near)
(entity? link)
(entity? far))
(let [properties (-> link :content :properties vals)
links (apply
merge
(map
#(hash-map (keyword (-> % :attrs :entity)) %)
(filter #(-> % :attrs :entity) properties)))
near-name (-> near :attrs :name)
link-name (-> link :attrs :name)
far-name (-> far :attrs :name)
pretty-far (singularise far-name)
query-name (str "list-" link-name "-" near-name "-by-" pretty-far)
signature ":? :*"]
(hash-map
(keyword query-name)
{:name query-name
:signature signature
:entity link
:type :select-many-to-many
:near-entity near
:far-entity far
:query
(s/join
"\n"
(remove
empty?
(list
(str "-- :name " query-name " " signature)
(str "-- :doc lists all existing " near-name " records related through " link-name " to a given " pretty-far )
(str "SELECT "near-name ".*")
(str "FROM " near-name ", " link-name )
(str "WHERE " near-name "." (first (key-names near)) " = " link-name "." (singularise near-name) "_id" )
("\tAND " link-name "." (singularise far-name) "_id = :id")
(order-by-clause near))))}))))
(defn link-table-queries [entity application]
"Generate all the link queries in this `application` which link via this `entity`."
(let
[entities (map
;; find the far-side entities
(fn
[far-name]
(children
application
(fn [x]
(and
(= (:tag x) :entity)
(= (:name (:attrs x)) far-name)))))
;; of those properties of this `entity` which are of type `entity`
(remove
nil?
(map
#(-> % :attrs :entity)
(children entity #(= (:tag %) :property)))))
pairs (combinations entities 2)]
(apply
merge
(map
#(merge
(link-table-query (nth % 0) entity (nth % 1))
(link-table-query (nth % 1) entity (nth % 0)))
pairs))))
(defn delete-query [entity] (defn delete-query [entity]
"Generate an appropriate `delete` query for this `entity`" "Generate an appropriate `delete` query for this `entity`"
@ -405,7 +354,7 @@
(delete-query entity) (delete-query entity)
(select-query entity) (select-query entity)
(list-query entity) (list-query entity)
(search-query entity) (search-query entity application)
(foreign-queries entity application))) (foreign-queries entity application)))
([application] ([application]
(apply (apply
@ -417,10 +366,11 @@
(defn to-hugsql-queries (defn to-hugsql-queries
"Generate all [HugSQL](https://www.hugsql.org/) queries implied by this ADL `application` spec." "Generate all [HugSQL](https://www.hugsql.org/) queries implied by this ADL `application` spec."
[application] [application]
(let [file-path (str *output-path* "resources/sql/queries.sql")] (let [filepath (str *output-path* "resources/sql/queries.auto.sql")]
(make-parents file-path) (make-parents filepath)
(try
(spit (spit
file-path filepath
(s/join (s/join
"\n\n" "\n\n"
(cons (cons
@ -434,5 +384,16 @@
(sort (sort
#(compare (:name %1) (:name %2)) #(compare (:name %1) (:name %2))
(vals (vals
(queries application))))))))) (queries application)))))))
(if (> *verbosity* 0)
(println (str "\tGenerated " filepath)))
(catch
Exception any
(println
(str
"ERROR: Exception "
(.getName (.getClass any))
(.getMessage any)
" while printing "
filepath))))))

View file

@ -7,7 +7,7 @@
[clojure.xml :as x] [clojure.xml :as x]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f] [clj-time.format :as f]
[adl.utils :refer :all] [adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [queries]])) [adl.to-hugsql-queries :refer [queries]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -37,7 +37,6 @@
;;; to-hugsql-queries, because essentially we need one JSON entry point to wrap ;;; to-hugsql-queries, because essentially we need one JSON entry point to wrap
;;; each query. ;;; each query.
(defn file-header [application] (defn file-header [application]
(list (list
'ns 'ns
@ -47,7 +46,9 @@
(f/unparse (f/formatters :basic-date-time) (t/now))) (f/unparse (f/formatters :basic-date-time) (t/now)))
(list (list
:require :require
'[adl-support.core :as support]
'[clojure.java.io :as io] '[clojure.java.io :as io]
'[clojure.core.memoize :as memo]
'[compojure.core :refer [defroutes GET POST]] '[compojure.core :refer [defroutes GET POST]]
'[hugsql.core :as hugsql] '[hugsql.core :as hugsql]
'[noir.response :as nresponse] '[noir.response :as nresponse]
@ -60,24 +61,45 @@
(cons 'declare (sort (map #(symbol (name %)) (keys handlers-map))))) (cons 'declare (sort (map #(symbol (name %)) (keys handlers-map)))))
(defn generate-handler-body
"Generate and return the function body for the handler for this `query`."
[query]
(list
[{:keys ['params]}]
(list 'do (list (symbol (str "db/" (:name query))) 'params))
(case
(:type query)
(:delete-1 :update-1)
'(response/found "/")
nil)))
(defn generate-handler-src (defn generate-handler-src
"Generate and return the handler for this `query`."
[handler-name query-map method doc] [handler-name query-map method doc]
(hash-map (hash-map
:method method :method method
:src :src (remove
(remove
nil? nil?
(if
(or
(zero? (volatility (:entity query-map)))
(#{:delete-1 :insert-1 :update-1} (:type query-map)))
(concat
(list (list
'defn 'defn
handler-name handler-name
(str "Auto-generated method to " doc) (str "Auto-generated method to " doc))
[{:keys ['params]}] (generate-handler-body query-map))
(list 'do (list (symbol (str "db/" (:name query-map))) 'params)) (concat
(case (list
(:type query-map) 'def
(:delete-1 :update-1) handler-name
'(response/found "/") (list
nil))))) 'memo/ttl
(cons 'fn (generate-handler-body query-map))
:ttl/threshold
(* (volatility (:entity query-map)) 1000))))))))
(defn handler (defn handler
@ -100,7 +122,7 @@
(str "delete one record from the `" (str "delete one record from the `"
(-> query :entity :attrs :name) (-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `" "` table. Expects the following key(s) to be present in `params`: `"
(doall (-> query :entity :content :key :content keys)) (-> query :entity key-names)
"`.")) "`."))
:insert-1 :insert-1
(generate-handler-src (generate-handler-src
@ -108,9 +130,12 @@
(str "insert one record to the `" (str "insert one record to the `"
(-> query :entity :attrs :name) (-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `" "` table. Expects the following key(s) to be present in `params`: `"
(pr-str (-> query :entity :content :properties keys)) (pr-str
(map
#(keyword (:name (:attrs %)))
(-> query :entity insertable-properties )))
"`. Returns a map containing the keys `" "`. Returns a map containing the keys `"
(pr-str (-> query :entity :content :key :content keys)) (-> query :entity key-names)
"` identifying the record created.")) "` identifying the record created."))
:update-1 :update-1
(generate-handler-src (generate-handler-src
@ -121,10 +146,12 @@
(pr-str (pr-str
(distinct (distinct
(sort (sort
(map
#(keyword (:name (:attrs %)))
(flatten (flatten
(cons (cons
(-> query :entity :content :properties keys) (-> query :entity key-properties)
(-> query :entity :content :key :content keys)))))) (-> query :entity insertable-properties)))))))
"`.")) "`."))
:select-1 :select-1
(generate-handler-src (generate-handler-src
@ -132,15 +159,9 @@
(str "select one record from the `" (str "select one record from the `"
(-> query :entity :attrs :name) (-> query :entity :attrs :name)
"` table. Expects the following key(s) to be present in `params`: `" "` table. Expects the following key(s) to be present in `params`: `"
(pr-str (-> query :entity :content :key :content keys)) (-> query :entity key-names)
"`. Returns a map containing the following keys: `" "`. Returns a map containing the following keys: `"
(pr-str (map #(keyword (:name (:attrs %))) (-> query :entity all-properties))
(distinct
(sort
(flatten
(cons
(-> query :entity :content :properties keys)
(-> query :entity :content :key :content keys))))))
"`.")) "`."))
:select-many :select-many
(generate-handler-src (generate-handler-src
@ -149,26 +170,21 @@
(-> query :entity :attrs :name) (-> 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: `" "` 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 (pr-str
(distinct (map
(sort #(keyword (:name (:attrs %)))
(flatten (-> query :entity all-properties)))
(cons
(-> query :entity :content :properties keys)
(-> query :entity :content :key :content keys))))))
"`.")) "`."))
:text-search :text-search
(generate-handler-src (generate-handler-src
handler-name query :get handler-name query :get
(str "select all records from the `" (str "select all records from the `"
(-> query :entity :attrs :name) (-> 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: `" "` 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 (pr-str
(distinct (map
(sort #(keyword (:name (:attrs %)))
(flatten (-> query :entity all-properties)))
(cons
(-> query :entity :content :properties keys)
(-> query :entity :content :key :content keys))))))
"`.")) "`."))
(:select-many-to-many (:select-many-to-many
:select-one-to-many) :select-one-to-many)
@ -221,25 +237,31 @@
(defn to-json-routes (defn to-json-routes
[application] [application]
(let [handlers-map (make-handlers-map application) (let [handlers-map (make-handlers-map application)
filepath (str *output-path* (:name (:attrs application)) "/routes/auto_json.clj")] filepath (str *output-path* "src/clj/" (:name (:attrs application)) "/routes/auto_json.clj")]
(make-parents filepath) (make-parents filepath)
(try
(with-open [output (writer filepath)] (with-open [output (writer filepath)]
(binding [*out* output] (binding [*out* output]
(doall (pprint (file-header application))
(map (println)
(fn [f]
(pprint f)
(println "\n"))
(list
(file-header application)
(declarations handlers-map)
(defroutes handlers-map))))
(doall (doall
(map (map
(fn [h] (fn [h]
(pprint (:src (handlers-map h))) (pprint (:src (handlers-map h)))
(println) (println)
h) h)
(sort (keys handlers-map)))))))) (sort (keys handlers-map))))
(pprint (defroutes handlers-map))))
(if (> *verbosity* 0)
(println (str "\tGenerated " filepath)))
(catch
Exception any
(println
(str
"ERROR: Exception "
(.getName (.getClass any))
(.getMessage any)
" while printing "
filepath))))))

View file

@ -7,7 +7,7 @@
[clojure.xml :as x] [clojure.xml :as x]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f] [clj-time.format :as f]
[adl.utils :refer :all] [adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [queries]])) [adl.to-hugsql-queries :refer [queries]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -346,7 +346,7 @@
(safe-name (first (key-names farside)) :sql)))) (safe-name (first (key-names farside)) :sql))))
entity-fields)))) entity-fields))))
";" ";"
(emit-permissions-grant view-name :SELECT (permissions entity application)))))))) (emit-permissions-grant view-name :SELECT (find-permissions entity application))))))))
(defn emit-referential-integrity-link (defn emit-referential-integrity-link
@ -447,8 +447,8 @@
(defn construct-link-property (defn construct-link-property
[entity] [entity]
{:tag :property {:tag :property
:attrs {:name (safe-name (str (:name (:attrs entity)) "_id") :sql) :attrs {:name (safe-name (str (singularise (:name (:attrs entity))) "_id") :sql)
:column (safe-name (str (:name (:attrs entity)) "_id") :sql) :column (safe-name (str (singularise (:name (:attrs entity))) "_id") :sql)
:type "entity" :type "entity"
:entity (:name (:attrs entity)) :entity (:name (:attrs entity))
:farkey (safe-name (first (key-names entity)) :sql)}}) :farkey (safe-name (first (key-names entity)) :sql)}})
@ -564,10 +564,22 @@
[application] [application]
(let [filepath (str (let [filepath (str
*output-path* *output-path*
"/resources/sql/" "resources/sql/"
(:name (:attrs application)) (:name (:attrs application))
".postgres.sql")] ".postgres.sql")]
(make-parents filepath) (make-parents filepath)
(spit filepath (emit-application application)))) (try
(spit filepath (emit-application application))
(if (> *verbosity* 0)
(println (str "\tGenerated " filepath)))
(catch
Exception any
(println
(str
"ERROR: Exception "
(.getName (.getClass any))
(.getMessage any)
" while printing "
filepath))))))

View file

@ -1,5 +1,5 @@
(ns adl.to-reframe (ns adl.to-reframe
(:require [adl.utils :refer :all] (:require [adl-support.utils :refer :all]
[clojure.string :as s] [clojure.string :as s]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f])) [clj-time.format :as f]))
@ -27,6 +27,9 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; TODO: not anywhere near finished.
(defn file-header (defn file-header
([parent-name this-name extra-requires] ([parent-name this-name extra-requires]
(list 'ns (symbol (str parent-name ".views." this-name)) (list 'ns (symbol (str parent-name ".views." this-name))

View file

@ -1,13 +1,14 @@
(ns ^{:doc "Application Description Language: generate routes for user interface requests." (ns ^{:doc "Application Description Language: generate routes for user interface requests."
:author "Simon Brooke"} :author "Simon Brooke"}
adl.to-selmer-routes adl.to-selmer-routes
(:require [clojure.java.io :refer [file make-parents writer]] (:require [adl-support.utils :refer :all]
[clojure.java.io :refer [file make-parents writer]]
[clojure.pprint :refer [pprint]] [clojure.pprint :refer [pprint]]
[clojure.string :as s] [clojure.string :as s]
[clojure.xml :as x] [clojure.xml :as x]
[clj-time.core :as t] [clj-time.core :as t]
[clj-time.format :as f] [clj-time.format :as f]
[adl.utils :refer :all])) ))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
@ -32,7 +33,11 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Generally. there's one route in the generated file for each Selmer template which has been generated. ;;; Generally. there's one route in the generated file for each Selmer
;;; template which has been generated.
;;; TODO: there must be some more idiomatic way of generating all these
;;; functions.
(defn file-header (defn file-header
[application] [application]
@ -44,6 +49,7 @@
(f/unparse (f/formatters :basic-date-time) (t/now))) (f/unparse (f/formatters :basic-date-time) (t/now)))
(list (list
:require :require
'[adl-support.core :as support]
'[clojure.java.io :as io] '[clojure.java.io :as io]
'[compojure.core :refer [defroutes GET POST]] '[compojure.core :refer [defroutes GET POST]]
'[hugsql.core :as hugsql] '[hugsql.core :as hugsql]
@ -61,18 +67,24 @@
'defn 'defn
(symbol n) (symbol n)
(vector 'r) (vector 'r)
(list 'let (vector 'p (list :params 'r)) ;; TODO: we must take key params out of just params, (list 'let (vector
'p
(list 'support/massage-params (list :params 'r)))
;; TODO: we must take key params out of just params,
;; but we should take all other params out of form-params - because we need the key to ;; but we should take all other params out of form-params - because we need the key to
;; load the form in the first place, but just accepting values of other params would ;; load the form in the first place, but just accepting values of other params would
;; allow spoofing. ;; allow spoofing.
(list (list
'l/render 'l/render
(list 'resolve-template (str n ".html")) (list 'support/resolve-template (str n ".html"))
'(:session r)
(merge (merge
{:title (capitalise (:name (:attrs f))) {:title (capitalise (:name (:attrs f)))
:params 'p} :params 'p}
(case (:tag f) (case (:tag f)
(:form :page) (:form :page)
(reduce
merge
{:record {:record
(list 'if (list 'empty? (list 'remove 'nil? (list 'vals 'p))) [] (list 'if (list 'empty? (list 'remove 'nil? (list 'vals 'p))) []
(list (list
@ -80,6 +92,13 @@
(str "db/get-" (singularise (:name (:attrs e))))) (str "db/get-" (singularise (:name (:attrs e)))))
(symbol "db/*db*") (symbol "db/*db*")
'p))} 'p))}
(map
(fn [p]
(hash-map
(keyword (-> p :attrs :entity))
(list (symbol (str "db/list-" (:entity (:attrs p)))) (symbol "db/*db*"))))
(filter #(#{"entity" "link"} (:type (:attrs %)))
(descendants-with-tag e :property))))
:list :list
{:records {:records
(list (list
@ -133,7 +152,7 @@
'auto-selmer-routes 'auto-selmer-routes
(cons (cons
'(GET '(GET
"/index" "/admin"
request request
(route/restricted (route/restricted
(apply (resolve-handler "index") (list request)))) (apply (resolve-handler "index") (list request))))
@ -167,25 +186,19 @@
(defn to-selmer-routes (defn to-selmer-routes
[application] [application]
(let [filename (str *output-path* (:name (:attrs application)) "/routes/auto.clj")] (let [filepath (str *output-path* "src/clj/" (:name (:attrs application)) "/routes/auto.clj")]
(make-parents filename) (make-parents filepath)
(with-open [output (writer filename)] (try
(with-open [output (writer filepath)]
(binding [*out* output] (binding [*out* output]
(pprint (file-header application)) (pprint (file-header application))
(println) (println)
(pprint '(defn raw-resolve-template [n]
(if
(.exists (io/as-file (str "resources/templates/" n)))
n
(str "auto/" n))))
(println)
(pprint '(def resolve-template (memoize raw-resolve-template)))
(println)
(pprint '(defn index (pprint '(defn index
[r] [r]
(l/render (l/render
(resolve-template (support/resolve-template
"application-index.html") "application-index.html")
(:session r)
{:title "Administrative menu"}))) {:title "Administrative menu"})))
(println) (println)
(doall (doall
@ -197,7 +210,9 @@
(pprint (make-handler c e application)) (pprint (make-handler c e application))
(println)) (println))
(filter (fn [c] (#{:form :list :page} (:tag c))) (children e))))) (filter (fn [c] (#{:form :list :page} (:tag c))) (children e)))))
(children-with-tag application :entity))) (sort
#(compare (:name (:attrs %1))(:name (:attrs %2)))
(children-with-tag application :entity))))
(pprint (pprint
(generate-handler-resolver application)) (generate-handler-resolver application))
(println) (println)
@ -205,5 +220,16 @@
(memoize raw-resolve-handler))) (memoize raw-resolve-handler)))
(println) (println)
(pprint (make-defroutes application)) (pprint (make-defroutes application))
(println))))) (println)))
(if (> *verbosity* 0)
(println (str "\tGenerated " filepath)))
(catch
Exception any
(println
(str
"ERROR: Exception "
(.getName (.getClass any))
(.getMessage any)
" while printing "
filepath))))))

View file

@ -1,8 +1,8 @@
(ns ^{:doc "Application Description Language - generate Selmer templates for the HTML pages implied by an ADL file." (ns ^{:doc "Application Description Language - generate Selmer templates for the HTML pages implied by an ADL file."
:author "Simon Brooke"} :author "Simon Brooke"}
adl.to-selmer-templates adl.to-selmer-templates
(:require [adl.utils :refer :all] (:require [adl-support.utils :refer :all]
[clojure.java.io :refer [file]] [clojure.java.io :refer [file make-parents]]
[clojure.pprint :as p] [clojure.pprint :as p]
[clojure.string :as s] [clojure.string :as s]
[clojure.xml :as x] [clojure.xml :as x]
@ -127,7 +127,6 @@
#(and #(and
(= (:tag %) :prompt) (= (:tag %) :prompt)
(= (:locale :attrs %) *locale*)))) (= (:locale :attrs %) *locale*))))
(:name (:attrs field-or-property)) (:name (:attrs field-or-property))
(:property (:attrs field-or-property))))) (:property (:attrs field-or-property)))))
@ -140,7 +139,8 @@
(defn save-widget (defn save-widget
"Return an appropriate 'save' widget for this `form` operating on this `entity` taken "Return an appropriate 'save' widget for this `form` operating on this `entity` taken
from this `application`." from this `application`.
TODO: should be suppressed unless a member of a group which can insert or edit."
[form entity application] [form entity application]
{:tag :p {:tag :p
:attrs {:class "widget action-safe"} :attrs {:class "widget action-safe"}
@ -151,13 +151,14 @@
:attrs {:id "save-button" :attrs {:id "save-button"
:name "save-button" :name "save-button"
:class "action-safe" :class "action-safe"
:type :submit :type "submit"
:value (str "Save!")}}]}) :value (str "Save!")}}]})
(defn delete-widget (defn delete-widget
"Return an appropriate 'save' widget for this `form` operating on this `entity` taken "Return an appropriate 'save' widget for this `form` operating on this `entity` taken
from this `application`." from this `application`.
TODO: should be suppressed unless member of a group which can delete."
[form entity application] [form entity application]
{:tag :p {:tag :p
:attrs {:class "widget action-dangerous"} :attrs {:class "widget action-dangerous"}
@ -168,7 +169,7 @@
:attrs {:id "delete-button" :attrs {:id "delete-button"
:name "delete-button" :name "delete-button"
:class "action-dangerous" :class "action-dangerous"
:type :submit :type "submit"
:value (str "Delete!")}}]}) :value (str "Delete!")}}]})
@ -254,22 +255,16 @@
:content (apply vector (get-options property form entity application))})))})) :content (apply vector (get-options property form entity application))})))}))
(defn permissions-for
[property entity application]
(first
(remove
empty?
(list
(children-with-tag property :permission)
(children-with-tag entity :permission)
(children-with-tag application :permission)))))
(defn compose-if-member-of-tag (defn compose-if-member-of-tag
[property entity application writable?] [property entity application writable?]
(let (let
[all-permissions (permissions-for property entity application) [all-permissions (find-permissions property entity application)
permissions (if writable? (writable-by all-permissions) (visible-to all-permissions))] permissions (map
s/lower-case
(if
writable?
(writable-by all-permissions)
(visible-to all-permissions)))]
(s/join (s/join
" " " "
(flatten (flatten
@ -290,20 +285,18 @@
property (if property (if
(= (:tag field-or-property) :property) (= (:tag field-or-property) :property)
field-or-property field-or-property
(first (child-with-tag entity
(children :property
entity #(= (:name (:attrs %))
#(and (:property (:attrs field-or-property)))))
(= (:tag %) :property) permissions (find-permissions field-or-property property form entity application)
(= (:name (:attrs %)) (:property (:attrs field-or-property)))))))
permissions (permissions property form entity application)
typedef (typedef property application) typedef (typedef property application)
visible-to (visible-to permissions) visible-to (visible-to permissions)
;; if the form isn't actually a form, no widget is writable. ;; if the form isn't actually a form, no widget is writable.
writable-by (if (= (:tag form) :form) (writable-by permissions)) writable-by (if (= (:tag form) :form) (writable-by permissions))
select? (#{"entity" "list" "link"} (:type (:attrs property)))] select? (#{"entity" "list" "link"} (:type (:attrs property)))]
(if (if
(formal-primary-key? property entity) (= (:distinct (:attrs property)) "system")
{:tag :input {:tag :input
:attrs {:id widget-name :attrs {:id widget-name
:name widget-name :name widget-name
@ -324,7 +317,18 @@
{:id widget-name {:id widget-name
:name widget-name :name widget-name
:type (widget-type property application typedef) :type (widget-type property application typedef)
:value (str "{{record." widget-name "}}")} :value (str "{{record." widget-name "}}")
:maxlength (:size (:attrs property))
:size (cond
(nil? (:size (:attrs property)))
"16"
(try
(> (read-string
(:size (:attrs property))) 60)
(catch Exception _ false))
"60"
true
(:size (:attrs property)))}
(if (if
(:minimum (:attrs typedef)) (:minimum (:attrs typedef))
{:min (:minimum (:attrs typedef))}) {:min (:minimum (:attrs typedef))})
@ -338,25 +342,21 @@
:name widget-name :name widget-name
:class "pseudo-widget disabled"} :class "pseudo-widget disabled"}
:content [(str "{{record." widget-name "}}")]} :content [(str "{{record." widget-name "}}")]}
"{% else %}"
{:tag :span
:attrs {:id widget-name
:name widget-name
:class "pseudo-widget not-authorised"}
:content [(str "You are not permitted to view " widget-name " of " (:name (:attrs entity)))]}
"{% endifmemberof %}" "{% endifmemberof %}"
"{% endifmemberof %}"]}))) "{% endifmemberof %}"]})))
(defn fields
[form]
(descendants-with-tag form :field))
(defn form-to-template (defn form-to-template
"Generate a template as specified by this `form` element for this `entity`, "Generate a template as specified by this `form` element for this `entity`,
taken from this `application`. If `form` is nill, generate a default form taken from this `application`. If `form` is nill, generate a default form
template for the entity." template for the entity."
[form entity application] [form entity application]
(let
[keyfields (children
;; there should only be one key; its keys are properties
(first (children entity #(= (:tag %) :key))))]
{:content {:content
{:tag :div {:tag :div
:attrs {:id "content" :class "edit"} :attrs {:id "content" :class "edit"}
@ -369,15 +369,18 @@
(csrf-widget) (csrf-widget)
(map (map
#(widget % form entity application) #(widget % form entity application)
keyfields) (children-with-tag (child-with-tag entity :key) :properties))
(map (map
#(widget % form entity application) #(widget % form entity application)
(remove (remove
#(= (:distict (:attrs %)) :system) #(let
(fields entity))) [property (filter
(fn [p] (= (:name (:attrs p)) (:property (:attrs %))))
(descendants-with-tag entity :property))]
(= (:distict (:attrs property)) :system))
(children-with-tag form :field)))
(save-widget form entity application) (save-widget form entity application)
(delete-widget form entity application)))}]}})) (delete-widget form entity application)))}]}})
(defn page-to-template (defn page-to-template
@ -402,9 +405,11 @@
"time" "time" "time" "time"
"text") "text")
base-name (:property (:attrs field)) base-name (:property (:attrs field))
search-name (if search-name (safe-name
(if
(= (:type (:attrs property)) "entity") (= (:type (:attrs property)) "entity")
(str base-name "_expanded") base-name)] (str base-name "_expanded") base-name)
:sql)]
(hash-map (hash-map
:tag :th :tag :th
:content :content
@ -430,7 +435,7 @@
#(hash-map #(hash-map
:content [(prompt %)] :content [(prompt %)]
:tag :th) :tag :th)
(fields list-spec)))} (children-with-tag list-spec :field)))}
{:tag :tr {:tag :tr
:content :content
(apply (apply
@ -438,7 +443,7 @@
(concat (concat
(map (map
#(compose-list-search-widget % entity) #(compose-list-search-widget % entity)
(fields list-spec)) (children-with-tag list-spec :field))
'({:tag :th '({:tag :th
:content :content
[{:tag :input [{:tag :input
@ -477,18 +482,19 @@
{:tag :td :content {:tag :td :content
(let (let
[p (first (filter #(= (:name (:attrs %)) (:property (:attrs field))) (all-properties entity))) [p (first (filter #(= (:name (:attrs %)) (:property (:attrs field))) (all-properties entity)))
s (safe-name (:name (:attrs p)) :sql)
e (first e (first
(filter (filter
#(= (:name (:attrs %)) (:entity (:attrs p))) #(= (:name (:attrs %)) (:entity (:attrs p)))
(children-with-tag application :entity))) (children-with-tag application :entity)))
c (str "{{ record." (:property (:attrs field)) " }}")] c (str "{{ record." s " }}")]
(if (if
(= (:type (:attrs p)) "entity") (= (:type (:attrs p)) "entity")
[{:tag :a [{:tag :a
:attrs {:href (edit-link e application (list (:name (:attrs p))))} :attrs {:href (edit-link e application (list (:name (:attrs p))))}
:content [(str "{{ record." (:property (:attrs field)) "_expanded }}")]}] :content [(str "{{ record." s "_expanded }}")]}]
[c]))}) [c]))})
(fields list-spec)) (children-with-tag list-spec :field))
[{:tag :td [{:tag :td
:content :content
[{:tag :a [{:tag :a
@ -610,15 +616,16 @@
(form-to-template nil entity application)}))))) (form-to-template nil entity application)})))))
(defn application-to-template (defn application-to-template
[application] [application]
(let (let
[first-class-entities (filter [first-class-entities
(sort-by
#(:name (:attrs %))
(filter
#(children-with-tag % :list) #(children-with-tag % :list)
(children-with-tag application :entity))] (children-with-tag application :entity)))]
{:content {:content
{:application-index
{:tag :dl {:tag :dl
:attrs {:class "index"} :attrs {:class "index"}
:content :content
@ -644,8 +651,7 @@
:tag :p :tag :p
:content (:content d))) :content (:content d)))
(children-with-tag % :documentation)))) (children-with-tag % :documentation))))
first-class-entities)))}}})) first-class-entities)))}}))
(defn write-template-file (defn write-template-file
@ -677,17 +683,29 @@
(file-footer filename application))))) (file-footer filename application)))))
(catch Exception any (catch Exception any
(spit (spit
(str *output-path* filename) filepath
(s/join
"\n"
(list
(file-header filename application)
(with-out-str (with-out-str
(println (x/emit-element template))
(str (file-footer filename application))))
"<!-- Exception " (if (> *verbosity* 0) (println "\tGenerated " filepath))
(catch Exception any
(let [report (str
"ERROR: Exception "
(.getName (.getClass any)) (.getName (.getClass any))
(.getMessage any) (.getMessage any)
" while printing " " while printing "
filename "-->")) filename)]
(p/pprint template)))))) (spit
filename) filepath
(with-out-str
(println (str "<!-- " report "-->"))
(p/pprint template)))
(println report)))))
(str filepath)))
(defn to-selmer-templates (defn to-selmer-templates
@ -708,12 +726,13 @@
(try (try
(write-template-file filename (templates-map %) application) (write-template-file filename (templates-map %) application)
(catch Exception any (catch Exception any
(println
(str (str
"Exception " "ERROR: Exception "
(.getName (.getClass any)) (.getName (.getClass any))
(.getMessage any) (.getMessage any)
" while writing " " while writing "
filename))))) filename))))))
(keys templates-map))))) (keys templates-map)))))

View file

@ -1,457 +0,0 @@
(ns ^{:doc "Application Description Language - utility functions."
:author "Simon Brooke"}
adl.utils
(:require [clojure.string :as s]
[clojure.pprint :as p]
[clojure.xml :as x]
[adl.validator :refer [valid-adl? validate-adl]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; adl.utils: utility functions.
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:dynamic *locale*
"The locale for which files will be generated."
"en-GB")
(def ^:dynamic *output-path*
"The path to which generated files will be written."
"resources/auto/")
(defn element?
"True if `o` is a Clojure representation of an XML element."
[o]
(and (map? o) (:tag o) (:attrs o)))
(defn wrap-lines
"Wrap lines in this `text` to this `width`; return a list of lines."
;; Shamelessly adapted from https://www.rosettacode.org/wiki/Word_wrap#Clojure
[width text]
(s/split-lines
(p/cl-format
nil
(str "~{~<~%~1," width ":;~A~> ~}")
(clojure.string/split text #" "))))
(defn emit-header
"Emit this `content` as a sequence of wrapped lines each prefixed with
`prefix`, and the whole delimited by rules."
[prefix & content]
(let [comment-rule (apply str (repeat 70 (last prefix)))
p (str "\n" prefix "\t") ]
(str
prefix
comment-rule
p
(s/join
p
(flatten
(interpose
""
(map
#(wrap-lines 70 (str %))
(flatten content)))))
"\n"
prefix
comment-rule)))
(defn sort-by-name
[elements]
(sort #(compare (:name (:attrs %1)) (:name (:attrs %2))) elements))
(defn link-table-name
"Canonical name of a link table between entity `e1` and entity `e2`."
[e1 e2]
(s/join
"_"
(cons
"ln"
(sort
(list
(:name (:attrs e1)) (:name (:attrs e2)))))))
(defn children
"Return the children of this `element`; if `predicate` is passed, return only those
children satisfying the predicate."
([element]
(if
(keyword? (:tag element)) ;; it has a tag; it seems to be an XML element
(:content element)))
([element predicate]
(filter
predicate
(children element))))
(defn child
"Return the first child of this `element` satisfying this `predicate`."
[element predicate]
(first (children element predicate)))
(defn attributes
"Return the attributes of this `element`; if `predicate` is passed, return only those
attributes satisfying the predicate."
([element]
(if
(keyword? (:tag element)) ;; it has a tag; it seems to be an XML element
(:attrs element)))
([element predicate]
(filter
predicate
(attributes element))))
(defn typedef
"If this `property` is of type `defined`, return its type definition from
this `application`, else nil."
[property application]
(if
(= (:type (:attrs property)) "defined")
(child
application
#(and
(= (:tag %) :typedef)
(= (:name (:attrs %)) (:typedef (:attrs property)))))))
(defn permissions
"Return appropriate permissions of this `property`, taken from this `entity` of this
`application`, in the context of this `page`."
([property page entity application]
(first
(remove
empty?
(list
(children page #(= (:tag %) :permission))
(children property #(= (:tag %) :permission))
(children entity #(= (:tag %) :permission))
(children application #(= (:tag %) :permission))))))
([property entity application]
(permissions property nil entity application))
([entity application]
(permissions nil nil entity application)))
(defn permission-groups
"Return a list of names of groups to which this `predicate` is true of
some permission taken from these `permissions`, else nil."
[permissions predicate]
(let [groups (remove
nil?
(map
#(if
(apply predicate (list %))
(:group (:attrs %)))
permissions))]
(if groups groups)))
(defn formal-primary-key?
"Does this `prop-or-name` appear to be a property (or the name of a property)
which is a formal primary key of this entity?"
[prop-or-name entity]
(if
(map? prop-or-name)
(formal-primary-key? (:name (:attrs prop-or-name)) entity)
(let [primary-key (first (children entity #(= (:tag %) :key)))
property (first
(children
primary-key
#(and
(= (:tag %) :property)
(= (:name (:attrs %)) prop-or-name))))]
(= (:distinct (:attrs property)) "system"))))
(defn entity?
"Return true if `x` is an ADL entity."
[x]
(= (:tag x) :entity))
(defn property?
"True if `o` is a property."
[o]
(= (:tag o) :property))
(defn entity-for-property
"If this `property` references an entity, return that entity from this `application`"
[property application]
(if
(and (property? property) (:entity (:attrs property)))
(child
application
#(and
(entity? %)
(= (:name (:attrs %))(:entity (:attrs property)))))))
(defn visible-to
"Return a list of names of groups to which are granted read access,
given these `permissions`, else nil."
[permissions]
(permission-groups permissions #(#{"read" "insert" "noedit" "edit" "all"} (:permission (:attrs %)))))
(defn writable-by
"Return a list of names of groups to which are granted write access,
given these `permissions`, else nil.
TODO: TOTHINKABOUT: properties are also writable by `insert` and `noedit`, but only if the
current value is nil."
[permissions]
(permission-groups permissions #(#{"edit" "all"} (:permission (:attrs %)))))
(defn singularise
"Attempt to construct an idiomatic English-language singular of this string."
[string]
(cond
(.endsWith string "ss") string
(.endsWith string "ise") string
true
(s/replace
(s/replace
(s/replace
(s/replace string #"_" "-")
#"s$" "")
#"se$" "s")
#"ie$" "y")))
(defn capitalise
"Return a string like `s` but with each token capitalised."
[s]
(s/join
" "
(map
#(apply str (cons (Character/toUpperCase (first %)) (rest %)))
(s/split s #"[ \t\r\n]+"))))
(defn pretty-name
[entity]
(capitalise (singularise (:name (:attrs entity)))))
(defn safe-name
"Return a safe name for the object `o`, given the specified `convention`.
`o` is expected to be either a string or an entity."
([o]
(if
(element? o)
(safe-name (:name (:attrs o)))
(s/replace (str o) #"[^a-zA-Z0-9-]" "")))
([o convention]
(if
(element? o)
(safe-name (:name (:attrs o)) convention)
(let [string (str o)]
(case convention
(:sql :c) (s/replace string #"[^a-zA-Z0-9_]" "_")
:c-sharp (s/replace (capitalise string) #"[^a-zA-Z0-9]" "")
:java (let
[camel (s/replace (capitalise string) #"[^a-zA-Z0-9]" "")]
(apply str (cons (Character/toLowerCase (first camel)) (rest camel))))
(safe-name string))))))
(defn read-adl [url]
(let [adl (x/parse url)
valid? (valid-adl? adl)]
(if valid? adl
(throw (Exception. (str (validate-adl adl)))))))
(defn children-with-tag
"Return all children of this `element` which have this `tag`;
if `element` is `nil`, return `nil`."
[element tag]
(if
element
(children element #(= (:tag %) tag))))
(defn child-with-tag
"Return the first child of this `element` which has this `tag`;
if `element` is `nil`, return `nil`."
[element tag]
(first (children-with-tag element tag)))
(defmacro properties
"Return all the properties of this `entity`."
[entity]
`(children-with-tag ~entity :property))
(defn descendants-with-tag
"Return all descendants of this `element`, recursively, which have this `tag`."
[element tag]
(flatten
(remove
empty?
(cons
(children element #(= (:tag %) tag))
(map
#(descendants-with-tag % tag)
(children element))))))
(defn insertable?
"Return `true` it the value of this `property` may be set from user-supplied data."
[property]
(and
(= (:tag property) :property)
(not (#{"link"} (:type (:attrs property))))
(not (= (:distinct (:attrs property)) "system"))))
(defmacro all-properties
"Return all properties of this `entity` (including key properties)."
[entity]
`(descendants-with-tag ~entity :property))
(defn user-distinct-properties
"Return the properties of this `entity` which are user distinct"
[entity]
(filter #(#{"user" "all"} (:distinct (:attrs %))) (all-properties entity)))
(defmacro insertable-properties
"Return all the properties of this `entity` (including key properties) into
which user-supplied data can be inserted"
[entity]
`(filter
insertable?
(all-properties ~entity)))
(defmacro key-properties
[entity]
`(children-with-tag (first (children-with-tag ~entity :key)) :property))
(defmacro insertable-key-properties
[entity]
`(filter insertable? (key-properties entity)))
(defn link-table?
"Return true if this `entity` represents a link table."
[entity]
(let [properties (all-properties entity)
links (filter #(-> % :attrs :entity) properties)]
(= (count properties) (count links))))
(defn key-names [entity]
(remove
nil?
(map
#(:name (:attrs %))
(key-properties entity))))
(defn base-type
[property application]
(cond
(:typedef (:attrs property))
(:type
(:attrs
(child
application
#(and
(= (:tag %) :typedef)
(= (:name (:attrs %)) (:typedef (:attrs property)))))))
(:entity (:attrs property))
(:type
(:attrs
(first
(key-properties
(child
application
#(and
(= (:tag %) :entity)
(= (:name (:attrs %)) (:entity (:attrs property)))))))))
true
(:type (:attrs property))))
(defn is-quotable-type?
"True if the value for this field should be quoted."
[property application]
(#{"date" "image" "string" "text" "time" "timestamp" "uploadable"} (base-type property application)))
(defn has-primary-key? [entity]
(> (count (key-names entity)) 0))
(defn has-non-key-properties? [entity]
(>
(count (all-properties entity))
(count (key-properties entity))))
(defn distinct-properties
[entity]
(filter
#(#{"system" "all"} (:distinct (:attrs %)))
(properties entity)))
(defn path-part
"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`
ADL element, or may be one of the keywords `:form`, `:list`, `:page` in which case the
first child of the `entity` of the specified type will be used."
[form entity application]
(cond
(and (map? form) (#{:list :form :page} (:tag form)))
(s/join
"-"
(flatten
(list
(name (:tag form)) (:name (:attrs entity)) (s/split (:name (:attrs form)) #"[ \n\r\t]+"))))
(keyword? form)
(path-part (first (children-with-tag entity form)) entity application)))
(defn editor-name
"Return the path-part of the editor form for this `entity`. Note:
assumes the editor form is the first form listed for the entity."
[entity application]
(path-part :form entity application))
(defn type-for-defined
[property application]
(:type (:attrs (typedef property application))))

View file

@ -1,7 +1,9 @@
(ns ^{:doc "Application Description Language: validator for ADL structure." (ns ^{:doc "Application Description Language: validator for ADL structure."
:author "Simon Brooke"} :author "Simon Brooke"}
adl.validator adl.validator
(:require [clojure.set :refer [union]] (:require [adl-support.utils :refer :all]
[clojure.set :refer [union]]
[clojure.xml :refer [parse]]
[bouncer.core :as b] [bouncer.core :as b]
[bouncer.validators :as v])) [bouncer.validators :as v]))
@ -28,44 +30,55 @@
;;;; ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; TODO: more work needed; I *think* this is finding spurious errors, and in any
;;; case it is failing to usefully locate the errors it is finding, so its
;;; diagnostic usefulness is small.
(defn disjunct-valid?
(defn try-validate
[o validation]
(if
(symbol? validation)
(try
(b/validate o validation)
(catch java.lang.ClassCastException c
;; 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]))
(catch Exception e
[{:error (.getName (.getClass e))
:message (.getMessage e)
:validation validation
:context o} o]))
[(str "Error: not a symbol" validation) o]))
(defmacro disjunct-valid?
;; Yes, this is a horrible hack. I should be returning the error structure ;; 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`. ;; 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 ;; 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? ;; do we identify the one which ought not to have failed?
[o & validations] [o & validations]
(println `(println
(str (str
(if (:tag o) (str "Tag: " (:tag o) "; ")) (if (:tag ~o) (str "Tag: " (:tag ~o) "; "))
(if (:name (:attrs o)) (str "Name: " (:name (:attrs o)) ";")) (if (:name (:attrs ~o)) (str "Name: " (:name (:attrs ~o)) ";"))
(if-not (or (:tag o) (:name (:attrs o))) (str "Context: " o)))) (if-not (or (:tag ~o) (:name (:attrs ~o))) (str "Context: " ~o))))
(let `(empty?
[rs (map (remove :tag (remove nil? (map first (map
#(try #(try-validate ~o '%)
(b/validate o %) ~validations))))))
(catch java.lang.ClassCastException c ;; ]
;; The validator regularly barfs on strings, which are perfectly ;; ;; if *any* succeeded, we succeeded
;; valid content of some elements. I need a way to validate ;; ;; otherwise, one of these is the valid error - but which? The answer, in my case
;; elements where they're not tolerated! ;; ;; is that if there is any which did not fail on the :tag check, then that is the
[nil o]) ;; ;; interesting one. But generally?
(catch Exception e ;; (try
[{:exception (.getMessage e) ;; (doall (map #(println (str "ERROR: " %)) suspicious))
:class (type e) ;; (empty? suspicious)
:context o} o])) ;; (catch Exception _ (println "ERROR while trying to print errors")
validations) ;; true))))
all-candidates (remove nil? (map first rs))
suspicious (remove :tag all-candidates)]
;; if *any* succeeded, we succeeded
;; otherwise, one of these is the valid error - but which? The answer, in my case
;; is that if there is any which did not fail on the :tag check, then that is the
;; interesting one. But generally?
(try
(doall (map #(println (str "\tError: " %)) suspicious))
(empty? suspicious)
(catch Exception _ (println "Error while trying to print errors")
true))))
;;; the remainder of this file is a fairly straight translation of the ADL 1.4 DTD into Clojure ;;; the remainder of this file is a fairly straight translation of the ADL 1.4 DTD into Clojure
@ -440,14 +453,15 @@
[:attrs :column] v/string [:attrs :column] v/string
[:attrs :concrete] [[v/member #{"true", "false"}]] [:attrs :concrete] [[v/member #{"true", "false"}]]
[:attrs :cascade] [[v/member cascade-actions]] [:attrs :cascade] [[v/member cascade-actions]]
:content [[v/every #(disjunct-valid? % ;; :content [[v/every #(disjunct-valid? %
documentation-validations ;; documentation-validations
generator-validations ;; generator-validations
permission-validations ;; permission-validations
option-validations ;; option-validations
prompt-validations ;; prompt-validations
help-validations ;; help-validations
ifmissing-validations)]]}) ;; ifmissing-validations)]]
})
(def permission-validations (def permission-validations
@ -657,3 +671,8 @@
(defn validate-adl [src] (defn validate-adl [src]
(b/validate src application-validations)) (b/validate src application-validations))
(defn validate-adl-file [filepath]
(validate-adl (parse filepath)))