Much progress! Many working!

This commit is contained in:
Simon Brooke 2018-06-10 21:05:02 +01:00
parent 538406b473
commit 398e3c3a6c
7 changed files with 634 additions and 241 deletions

View file

@ -418,6 +418,7 @@
<xsl:attribute name="property"> <xsl:attribute name="property">
<xsl:value-of select="@name"/> <xsl:value-of select="@name"/>
</xsl:attribute> </xsl:attribute>
<xsl:copy-of select="*"/>
</field> </field>
</xsl:for-each> </xsl:for-each>
</xsl:template> </xsl:template>
@ -435,6 +436,7 @@
<xsl:attribute name="property"> <xsl:attribute name="property">
<xsl:value-of select="@name"/> <xsl:value-of select="@name"/>
</xsl:attribute> </xsl:attribute>
<xsl:copy-of select="*"/>
</field> </field>
</xsl:otherwise> </xsl:otherwise>
</xsl:choose> </xsl:choose>
@ -454,6 +456,7 @@
</xsl:attribute> </xsl:attribute>
<xsl:apply-templates select="adl:prompt"/> <xsl:apply-templates select="adl:prompt"/>
<xsl:apply-templates select="adl:help"/> <xsl:apply-templates select="adl:help"/>
<xsl:copy-of select="*"/>
</field> </field>
</xsl:for-each> </xsl:for-each>
</xsl:template> </xsl:template>

View file

@ -36,17 +36,25 @@
"The path to which generated files will be written." "The path to which generated files will be written."
"resources/auto/") "resources/auto/")
(def electors {:tag :entity, :attrs {:magnitude "6", :name "electors", :table "electors"}, :content [{:tag :key, :attrs nil, :content [{:tag :property, :attrs {:distinct "system", :immutable "true", :column "id", :name "id", :type "integer", :required "true"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "id"}, :content nil}]}]} {:tag :property, :attrs {:distinct "user", :column "name", :name "name", :type "string", :required "true", :size "64"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "name"}, :content nil}]} {:tag :property, :attrs {:farkey "id", :entity "dwelling", :column "dwelling_id", :name "dwelling_id", :type "entity", :required "true"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "Flat"}, :content nil}]} {:tag :property, :attrs {:distinct "user", :column "phone", :name "phone", :type "string", :size "16"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "phone"}, :content nil}]} {:tag :property, :attrs {:distinct "user", :column "email", :name "email", :type "string", :size "128"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "email"}, :content nil}]} {:tag :property, :attrs {:default "Unknown", :farkey "id", :entity "genders", :column "gender", :type "entity", :name "gender"}, :content [{:tag :prompt, :attrs {:locale "en-GB", :prompt "gender"}, :content nil}]} {:tag :list, :attrs {:name "Electors", :properties "listed"}, :content [{:tag :field, :attrs {:property "id"}, :content nil} {:tag :field, :attrs {:property "name"}, :content nil} {:tag :field, :attrs {:property "dwelling_id"}, :content nil} {:tag :field, :attrs {:property "phone"}, :content nil} {:tag :field, :attrs {:property "email"}, :content nil} {:tag :field, :attrs {:property "gender"}, :content nil}]} {:tag :form, :attrs {:name "Elector", :properties "listed"}, :content [{:tag :field, :attrs {:property "id"}, :content nil} {:tag :field, :attrs {:property "name"}, :content nil} {:tag :field, :attrs {:property "dwelling_id"}, :content nil} {:tag :field, :attrs {:property "phone"}, :content nil} {:tag :field, :attrs {:property "email"}, :content nil} {:tag :field, :attrs {:property "gender"}, :content nil}]}]})
(defn where-clause (defn where-clause
"Generate an appropriate `where` clause for queries on this `entity`" "Generate an appropriate `where` clause for queries on this `entity`;
[entity] if `properties` are passed, filter on those properties, otherwise the key
properties."
([entity]
(where-clause entity (key-properties entity)))
([entity properties]
(let (let
[entity-name (:name (:attrs entity))] [entity-name (:name (:attrs entity))
property-names (map #(:name (:attrs %)) properties)]
(if
(not (empty? property-names))
(str (str
"WHERE " entity-name "." "WHERE "
(s/join (s/join
(str " AND\n\t" entity-name ".") "\n\tAND"
(map #(str % " = " (keyword %)) (key-names entity)))))) (map #(str entity-name "." % " = :" %) property-names)))))))
(defn order-by-clause (defn order-by-clause
@ -75,11 +83,7 @@
[entity] [entity]
(let [entity-name (:name (:attrs entity)) (let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
insertable-property-names (map insertable-property-names (map #(:name (:attrs %)) (insertable-properties entity))
#(:name (:attrs %))
(filter
#(not (= (:distinct (:attrs %)) "system"))
(all-properties entity)))
query-name (str "create-" pretty-name "!") query-name (str "create-" pretty-name "!")
signature ":! :n"] signature ":! :n"]
(hash-map (hash-map
@ -110,11 +114,7 @@
(has-non-key-properties? entity)) (has-non-key-properties? entity))
(let [entity-name (:name (:attrs entity)) (let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
property-names (remove property-names (map #(:name (:attrs %)) (insertable-properties entity))
nil?
(map
#(if (= (:tag %) :property) (:name (:attrs %)))
(vals (:properties (:content entity)))))
query-name (str "update-" pretty-name "!") query-name (str "update-" pretty-name "!")
signature ":! :n"] signature ":! :n"]
(hash-map (hash-map
@ -135,18 +135,12 @@
(defn search-query [entity] (defn search-query [entity]
"Generate an appropriate search query for this `entity`" "Generate an appropriate search query for string fields of this `entity`"
(let [entity-name (:name (:attrs entity)) (let [entity-name (:name (:attrs entity))
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"
props (concat (properties entity-map) (insertable-key-properties entity-map)) properties (all-properties entity)]
string-fields (filter
#(= (-> % :attrs :type) "string")
(children entity #(= (:tag %) :property)))]
(if
(empty? string-fields)
{}
(hash-map (hash-map
(keyword query-name) (keyword query-name)
{:name query-name {:name query-name
@ -168,21 +162,28 @@
"WHERE " "WHERE "
(s/join (s/join
"\n\tOR " "\n\tOR "
(filter
string?
(map (map
#(str (-> % :attrs :name) " LIKE '%:pattern%'") #(if
string-fields)) (#{"string" "date" "text"} (:type (:attrs %)))
(str (-> % :attrs :name) " LIKE '%:pattern%'"))
properties)))
(order-by-clause entity) (order-by-clause entity)
"--~ (if (:offset params) \"OFFSET :offset \")" "--~ (if (:offset params) \"OFFSET :offset \")"
"--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))) "--~ (if (:limit params) \"LIMIT :limit\" \"LIMIT 100\")")))})))
(defn select-query [entity] (defn select-query
"Generate an appropriate `select` query for this `entity`" "Generate an appropriate `select` query for this `entity`"
([entity properties]
(if (if
(has-primary-key? entity) (not (empty? properties))
(let [entity-name (:name (:attrs entity)) (let [entity-name (:name (:attrs entity))
pretty-name (singularise entity-name) pretty-name (singularise entity-name)
query-name (str "get-" pretty-name) query-name (if (= properties (key-properties entity))
(str "get-" pretty-name)
(str "get-" pretty-name "-by-" (s/join "=" (map #(:name (:attrs %)) properties))))
signature ":? :1"] signature ":? :1"]
(hash-map (hash-map
(keyword query-name) (keyword query-name)
@ -199,9 +200,20 @@
(str "-- :name " query-name " " signature) (str "-- :name " query-name " " signature)
(str "-- :doc selects an existing " pretty-name " record") (str "-- :doc selects an existing " pretty-name " record")
(str "SELECT * FROM " entity-name) (str "SELECT * FROM " entity-name)
(where-clause entity) (where-clause entity properties)
(order-by-clause entity))))})) (order-by-clause entity))))}))
{})) {}))
([entity]
(let [distinct-fields (distinct-properties entity)]
(apply
merge
(cons
(select-query entity (key-properties entity))
(map
#(select-query entity %)
(combinations distinct-fields (count distinct-fields))))))))
(select-query electors)
(defn list-query (defn list-query
@ -373,8 +385,9 @@
(defn queries (defn queries
"Generate all standard queries for this `entity` in this `application`." "Generate all standard queries for this `entity` in this `application`; if
[entity application] no entity is specified, generate all queris for the application."
([application entity]
(merge (merge
{} {}
(insert-query entity) (insert-query entity)
@ -383,11 +396,16 @@
(if (if
(link-table? entity) (link-table? entity)
(link-table-queries entity application) (link-table-queries entity application)
(merge {})
(select-query entity) (select-query entity)
(list-query entity) (list-query entity)
(search-query entity) (search-query entity)
(foreign-queries entity application))))) (foreign-queries entity application)))
([application]
(apply
merge
(map #(queries application %)
(children-with-tag application :entity)))))
(defn to-hugsql-queries (defn to-hugsql-queries
@ -410,11 +428,5 @@
(sort (sort
#(compare (:name %1) (:name %2)) #(compare (:name %1) (:name %2))
(vals (vals
(apply (queries application))))))))
merge
(map
#(queries % application)
(children
application
(fn [child] (= (:tag child) :entity))))))))))))

View file

@ -2,7 +2,6 @@
:author "Simon Brooke"} :author "Simon Brooke"}
adl.to-json-routes adl.to-json-routes
(:require [clojure.java.io :refer [file]] (:require [clojure.java.io :refer [file]]
[clojure.math.combinatorics :refer [combinations]]
[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]))

View file

@ -0,0 +1,152 @@
(ns ^{:doc "Application Description Language: generate routes for user interface requests."
:author "Simon Brooke"}
adl.to-selmer-routes
(:require [clojure.java.io :refer [file make-parents writer]]
[clojure.pprint :refer [pprint]]
[clojure.string :as s]
[clojure.xml :as x]
[clj-time.core :as t]
[clj-time.format :as f]
[adl.utils :refer :all]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; adl.to-selmer-routes: generate RING routes for REST requests.
;;;;
;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License
;;;; as published by the Free Software Foundation; either version 2
;;;; of the License, or (at your option) any later version.
;;;;
;;;; This program is distributed in the hope that it will be useful,
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;;; GNU General Public License for more details.
;;;;
;;;; You should have received a copy of the GNU General Public License
;;;; along with this program; if not, write to the Free Software
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
;;;; USA.
;;;;
;;;; Copyright (C) 2018 Simon Brooke
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Generally. there's one route in the generated file for each Selmer template which has been generated.
(def ^:dynamic *output-path*
"The path to which generated files will be written."
"resources/auto/")
(defn file-header
[application]
(list
'ns
(symbol (str (:name (:attrs application)) ".routes.auto"))
(str "JSON routes for " (pretty-name application)
" auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at "
(f/unparse (f/formatters :basic-date-time) (t/now)))
(list
:require
'[noir.response :as nresponse]
'[noir.util.route :as route]
'[compojure.core :refer [defroutes GET POST]]
'[ring.util.http-response :as response]
'[clojure.java.io :as io]
'[hugsql.core :as hugsql]
(vector (symbol (str (:name (:attrs application)) ".routes.manual")) :as 'm))))
(defn make-handler
[f e a]
(let [n (path-part f e a)]
(list
'defn
(symbol n)
(vector 'r)
(list
'layout/render
(list 'resolve-template (str n ".html")) {:title (pretty-name e)}))))
(defn make-route
"Make a route for method `m` to request the resource with name `n`."
[m n]
(list
m
(str "/" n)
'request
(list
'route/restricted
(list
'apply
(list 'resolve-handler n)
(list 'list 'request)))))
(defn make-defroutes
[application]
(let [routes (flatten
(map
(fn [e]
(map
(fn [c]
(path-part c e application))
(filter (fn [c] (#{:form :list :page} (:tag c))) (children e))))
(children-with-tag application :entity)))]
(cons
'defroutes
(cons
'auto-selmer-routes
(interleave
(map
(fn [r] (make-route 'GET r))
(sort routes))
(map
(fn [r] (make-route 'POST r))
(sort routes)))))))
(defn to-selmer-routes
[application]
(let [filename (str *output-path* (:name (:attrs application)) "/routes/auto.clj")]
(make-parents filename)
(with-open [output (writer filename)]
(binding [*out* output]
(pprint (file-header application))
(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 (memoise raw-resolve-template)))
(println)
(doall
(map
(fn [e]
(doall
(map
(fn [c]
(pprint (make-handler c e application))
(println))
(filter (fn [c] (#{:form :list :page} (:tag c))) (children e)))))
(children-with-tag application :entity)))
(pprint '(defn raw-resolve-handler
"Prefer the manually-written version of the handler with name `n`, if it exists, to the automatically generated one"
[n]
(let [s (symbol (str "m." n))]
(if
(bound? s)
(eval s)
(eval (symbol n))))))
(println)
(pprint '(def resolve-handler
(memoize raw-resolve-handler)))
(println)
(pprint (make-defroutes application))
(println)))))
(def x (x/parse "../youyesyet/youyesyet.canonical.adl.xml"))
(to-selmer-routes x)

View file

@ -1,9 +1,8 @@
(ns ^{;; :doc "Application Description Language - generate RING routes for REST requests." (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.utils :refer :all]
[clojure.java.io :refer [file]] [clojure.java.io :refer [file]]
[clojure.math.combinatorics :refer [combinations]]
[clojure.pprint :as p] [clojure.pprint :as p]
[clojure.string :as s] [clojure.string :as s]
[clojure.xml :as x] [clojure.xml :as x]
@ -13,7 +12,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; ;;;;
;;;; adl.to-json-routes: generate RING routes for REST requests. ;;;; adl.to-selmer-templates.
;;;; ;;;;
;;;; This program is free software; you can redistribute it and/or ;;;; This program is free software; you can redistribute it and/or
;;;; modify it under the terms of the GNU General Public License ;;;; modify it under the terms of the GNU General Public License
@ -43,6 +42,31 @@
"The path to which generated files will be written." "The path to which generated files will be written."
"resources/auto/") "resources/auto/")
(defn big-link
[content url]
{:tag :div
:attrs {:class "big-link-container"}
:content
[{:tag :a :attrs {:href url}
:content (if
(vector? content)
content
[content])}]})
(defn back-link
[content url]
{:tag :div
:attrs {:class "back-link-container"}
:content
[{:tag :a :attrs {:href url}
:content (if
(vector? content)
content
[content])}]})
(defn file-header (defn file-header
"Generate a header for a template file." "Generate a header for a template file."
[filename] [filename]
@ -67,7 +91,9 @@
"Return an appropriate prompt for the given `field-or-property` taken from this "Return an appropriate prompt for the given `field-or-property` taken from this
`form` of this `entity` of this `application`, in the context of the current `form` of this `entity` of this `application`, in the context of the current
binding of `*locale*`. TODO: something more sophisticated about i18n" binding of `*locale*`. TODO: something more sophisticated about i18n"
[field-or-property form entity application] ([field-or-property form entity application]
(prompt field-or-property))
([field-or-property]
(or (or
(first (first
(children (children
@ -75,7 +101,9 @@
#(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)))))
(defn csrf-widget (defn csrf-widget
@ -166,12 +194,48 @@
)))) ))))
(defn select-widget
[property form entity application]
(let [farname (:entity (:attrs property))
farside (first (children application #(= (:name (:attrs %)) farname)))
magnitude (try (read-string (:magnitude (:attrs farside))) (catch Exception _ 7))
async? (and (number? magnitude) (> magnitude 1))
widget-name (:name (:attrs property))]
{:tag :div
:attrs {:class "select-box" :farside farname :found (if farside "true" "false")}
:content
(apply
vector
(remove
nil?
(list
(if
async?
{:tag :input
:attrs
{:name (str widget-name "-search-box")
:onchange "/* javascript to repopulate the select widget */"}})
{:tag :select
:attrs (merge
{:id widget-name
:name widget-name}
(if
(= (:type (:attrs property)) "link")
{:multiple "multiple"})
(if
async?
{:comment "JavaScript stuff to fix up aynchronous loading"}))
:content (apply vector (get-options property form entity application))})))}))
(defn widget (defn widget
"Generate a widget for this `field-or-property` of this `form` for this `entity` "Generate a widget for this `field-or-property` of this `form` for this `entity`
taken from within this `application`." taken from within this `application`."
[field-or-property form entity application] [field-or-property form entity application]
(let (let
[widget-name (:name (:attrs field-or-property)) [widget-name (if (= (:tag field-or-property) :property)
(:name (:attrs field-or-property))
(:property (:attrs field-or-property)))
property (if property (if
(= (:tag field-or-property) :property) (= (:tag field-or-property) :property)
field-or-property field-or-property
@ -199,13 +263,11 @@
:content [{:tag :label :content [{:tag :label
:attrs {:for widget-name} :attrs {:for widget-name}
:content [(prompt field-or-property form entity application)]} :content [(prompt field-or-property form entity application)]}
"TODO: selmer command to hide for all groups except for those for which it is writable" (str "{% ifwritable " (:name (:attrs entity)) " " (:name (:attrs property)) " %}")
(if (cond
select? select?
{:tag :select (select-widget property form entity application)
:attrs {:id widget-name true
:name widget-name}
:content (get-options property form entity application)}
{:tag :input {:tag :input
:attrs (merge :attrs (merge
{:id widget-name {:id widget-name
@ -219,14 +281,20 @@
(:maximum (:attrs typedef)) (:maximum (:attrs typedef))
{:max (:maximum (:attrs typedef))}))}) {:max (:maximum (:attrs typedef))}))})
"{% else %}" "{% else %}"
"TODO: selmer if command to hide for all groups except to those for which it is readable" (str "{% ifreadable " (:name (:attrs entity)) " " (:name (:attrs property)) "%}")
{:tag :span {:tag :span
:attrs {:id widget-name :attrs {:id widget-name
:name widget-name :name widget-name
:class "pseudo-widget disabled"} :class "pseudo-widget disabled"}
:content [(str "{{record." widget-name "}}")]} :content [(str "{{record." widget-name "}}")]}
"{% endif %}" "{% endifreadable %}"
"{% endif %}"]}))) "{% endifwritable %}"]})))
(defn fields
[form]
(descendants-with-tag form :field))
(defn form-to-template (defn form-to-template
@ -235,22 +303,14 @@
template for the entity." template for the entity."
[form entity application] [form entity application]
(let (let
[name (str (if form (:name (:attrs form)) "edit") "-" (:name (:attrs entity))) [keyfields (children
keyfields (children
;; there should only be one key; its keys are properties ;; there should only be one key; its keys are properties
(first (children entity #(= (:tag %) :key)))) (first (children entity #(= (:tag %) :key))))]
fields (if
(and form (= "listed" (:properties (:attrs form))))
;; if we've got a form, collect its fields, fieldgroups and verbs
(flatten
(map #(if (#{:field :fieldgroup :verb} (:tag %)) %)
(children form)))
(children entity #(= (:tag %) :property)))]
{:tag :div {:tag :div
:attrs {:id "content" :class "edit"} :attrs {:id "content" :class "edit"}
:content :content
[{:tag :form [{:tag :form
:attrs {:action (str "{{servlet-context}}/" name) :attrs {:action (str "{{servlet-context}}/" (editor-name entity application))
:method "POST"} :method "POST"}
:content (flatten :content (flatten
(list (list
@ -260,7 +320,7 @@
keyfields) keyfields)
(map (map
#(widget % form entity application) #(widget % form entity application)
fields) (fields entity))
(save-widget form entity application) (save-widget form entity application)
(delete-widget form entity application)))}]})) (delete-widget form entity application)))}]}))
@ -273,67 +333,91 @@
[page entity application] [page entity application]
) )
(defn list-to-template
"Generate a template as specified by this `list` element for this `entity`, (defn- list-thead
taken from this `application`. If `list` is nill, generate a default list [list-spec]
template for the entity." {:tag :thead
[list-spec entity application]
(let [user-distinct-fields]
[:tag :div
:attrs {:id "content" :class "edit"}
:content :content
[:table {:caption (:name (:attrs entity))} [{:tag :tr
[:thead :content
[:tr (apply
vector
(map (map
#(vector :th (prompt %)) #(hash-map
(:fields list-spec))] :content [(prompt %)]
[tr :tag :th)
(fields list-spec)))}
{:tag :tr
:content
(apply
vector
(map (map
#(vector :th (prompt %)) #(hash-map
(:fields list-spec))] :tag :th
] :content
"{% for record in %records% %}" [{:tag :input
[:tr :attrs {:id (:property (:attrs %))
:name (:property (:attrs %))}}])
(fields list-spec)))}]})
(defn- list-tbody
[list-spec entity application]
{:tag :tbody
:content
["{% for record in %records% %}"
{:tag :tr
:content
(apply
vector
(concat
(map (map
(fn [field] (fn [field]
[:td (str "{% record." (:name (:attrs %)) " %}")]) {:tag :td :content [(str "{{ record." (:property (:attrs field)) " }}")]})
(:fields list-spec)) (fields list-spec))
[:td [{:tag :td
[:a :content
[{:tag :a
:attrs
{:href {:href
(str (str
"view-or-edit-" (editor-name entity application)
(:name (:attrs entity))
"?" "?"
(s/join (s/join
"&amp;" "&amp;"
(map (map
#(let [n (:name (:attrs %))] #(let [n (:name (:attrs %))]
(str n "=record." n))) (str n "={{ record." n "}}"))
(children (first (filter #(= (:tag %) :key) (children entity))))))} (children (first (filter #(= (:tag %) :key) (children entity)))))))}
View]]] :content ["View"]}]}]))}
"{% endfor %}" "{% endfor %}"]})
[:tfoot]]
(defn- list-to-template
"Generate a template as specified by this `list` element for this `entity`,
taken from this `application`. If `list` is nill, generate a default list
template for the entity."
[list-spec entity application]
{:tag :form
:attrs {:id "content" :class "list"}
:content
[(big-link (str "Add a new " (pretty-name entity)) (editor-name entity application))
{:tag :table
:attrs {:caption (:name (:attrs entity))}
:content
[
(list-thead list-spec)
(list-tbody list-spec entity application)
{:tag :tfoot}]}
"{% if offset > 0 %}" "{% if offset > 0 %}"
[:div {:id "back-link-container"} (back-link "Previous" "FIXME")
[:a {:href "FIXME"}
Previous]]
"{% endif %}" "{% endif %}"
[:div {:id "big-link-container"} (big-link "Next" "FIXME")
[:a {:href "FIXME"} (big-link (str "Add a new " (pretty-name entity)) (editor-name entity application))]})
Next]]
]))
]}))
(defn entity-to-templates (defn entity-to-templates
"Generate one or more templates for editing instances of this "Generate one or more templates for editing instances of this
`entity` in this `application`" `entity` in this `application`"
@ -349,37 +433,91 @@
(merge (merge
(if (if
forms forms
(apply merge (map #(assoc {} (keyword (str "form-" (:name (:attrs entity)) "-" (:name (:attrs %)))) (apply merge (map #(assoc {} (keyword (path-part % entity application))
(form-to-template % entity application)) (form-to-template % entity application))
forms)) forms))
{(keyword (str "form-" (:name (:attrs entity)))) {(keyword (str "form-" (:name (:attrs entity))))
(form-to-template nil entity application)}) (form-to-template nil entity application)})
(if (if
pages pages
(apply merge (map #(assoc {} (keyword (str "page-" (:name (:attrs entity)) "-" (:name (:attrs %)))) (apply merge (map #(assoc {} (keyword (path-part % entity application))
(page-to-template % entity application)) (page-to-template % entity application))
pages)) pages))
{(keyword (str "page-" (:name (:attrs entity)))) {(keyword (str "page-" (:name (:attrs entity))))
(page-to-template nil entity application)}) (page-to-template nil entity application)})
(if (if
lists lists
(apply merge (map #(assoc {} (keyword (str "list-" (:name (:attrs entity)) "-" (:name (:attrs %)))) (apply merge (map #(assoc {} (keyword (path-part % entity application))
(list-to-template % entity application)) (list-to-template % entity application))
lists)) lists))
{(keyword (str "list-" (:name (:attrs entity)))) {(keyword (str "list-" (:name (:attrs entity))))
(form-to-template nil entity application)}))))) (form-to-template nil entity application)})))))
(defn application-to-template
[application]
(let
[first-class-entities (filter
#(children-with-tag % :list)
(children-with-tag application :entity))]
{:application-index
{:tag :dl
:attrs {:class "index"}
:content
(apply
vector
(interleave
(map
#(hash-map
:tag :dt
:content
[{:tag :a
:attrs {:href (path-part :list % application)}
:content [(pretty-name %)]}])
first-class-entities)
(map
#(hash-map
:tag :dd
:content (apply
vector
(map
(fn [d]
(hash-map
:tag :p
:content (:content d)))
(children-with-tag % :documentation))))
first-class-entities)))}}))
(defn write-template-file (defn write-template-file
[filename template] [filename template]
(if
template
(try
(spit (spit
(str *output-path* filename) (str *output-path* filename)
(s/join (s/join
"\n" "\n"
(list (list
(file-header filename) (file-header filename)
(with-out-str (x/emit-element template)) (with-out-str
(file-footer filename))))) (x/emit-element template))
(file-footer filename))))
(catch Exception any
(spit
(str *output-path* filename)
(with-out-str
(println
(str
"<!-- Exception "
(.getName (.getClass any))
(.getMessage any)
" while printing "
filename "-->"))
(p/pprint template))))))
filename)
(defn to-selmer-templates (defn to-selmer-templates
@ -388,7 +526,7 @@
(let (let
[templates-map (reduce [templates-map (reduce
merge merge
{} (application-to-template application)
(map (map
#(entity-to-templates % application) #(entity-to-templates % application)
(children application #(= (:tag %) :entity))))] (children application #(= (:tag %) :entity))))]
@ -397,8 +535,15 @@
#(if #(if
(templates-map %) (templates-map %)
(let [filename (str (name %) ".html")] (let [filename (str (name %) ".html")]
(write-template-file filename (templates-map %)))) (try
(keys templates-map))) (write-template-file filename (templates-map %))
templates-map)) (catch Exception any
(str
"Exception "
(.getName (.getClass any))
(.getMessage any)
" while writing "
filename)))))
(keys templates-map)))))

View file

@ -36,11 +36,9 @@
(keyword? (:tag element)) ;; it has a tag; it seems to be an XML element (keyword? (:tag element)) ;; it has a tag; it seems to be an XML element
(:content element))) (:content element)))
([element predicate] ([element predicate]
(remove ;; there's a more idionatic way of doing remove-nil-map, but for the moment I can't recall it. (filter
nil? predicate
(map (children element))))
#(if (predicate %) %)
(children element)))))
(defn attributes (defn attributes
@ -51,11 +49,9 @@
(keyword? (:tag element)) ;; it has a tag; it seems to be an XML element (keyword? (:tag element)) ;; it has a tag; it seems to be an XML element
(:attrs element))) (:attrs element)))
([element predicate] ([element predicate]
(remove ;; there's a more idionatic way of doing remove-nil-map, but for the moment I can't recall it. (filter
nil? predicate
(map (attributes element))))
#(if (predicate %) %)
(:attrs element)))))
(defn typedef (defn typedef
@ -140,13 +136,33 @@
(defn singularise (defn singularise
"Attempt to construct an idiomatic English-language singular of this string." "Attempt to construct an idiomatic English-language singular of this string."
[string] [string]
(cond
(.endsWith string "ss") string
(.endsWith string "ise") string
true
(s/replace (s/replace
(s/replace (s/replace
(s/replace (s/replace
(s/replace string #"_" "-") (s/replace string #"_" "-")
#"s$" "") #"s$" "")
#"se$" "s") #"se$" "s")
#"ie$" "y")) #"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 link-table? (defn link-table?
@ -159,26 +175,8 @@
(defn read-adl [url] (defn read-adl [url]
(let [adl (x/parse url) (let [adl (x/parse url)
valid? (valid-adl? adl)] valid? (valid-adl? adl)]
adl)) (if valid? adl
;; (if valid? adl (throw (Exception. (str (validate-adl adl)))))))
;; (throw (Exception. (str (validate-adl adl)))))))
(defn key-names [entity-map]
(remove
nil?
(map
#(:name (:attrs %))
(vals (:content (:key (:content entity-map)))))))
(defn has-primary-key? [entity-map]
(> (count (key-names entity-map)) 0))
(defn has-non-key-properties? [entity-map]
(>
(count (vals (:properties (:content entity-map))))
(count (key-names entity-map))))
(defn children-with-tag (defn children-with-tag
@ -186,6 +184,11 @@
[element tag] [element tag]
(children element #(= (:tag %) tag))) (children element #(= (:tag %) tag)))
(defmacro properties
"Return all the properties of this `entity`."
[entity]
`(children-with-tag ~entity :property))
(defn descendants-with-tag (defn descendants-with-tag
"Return all descendants of this `element`, recursively, which have this `tag`." "Return all descendants of this `element`, recursively, which have this `tag`."
[element tag] [element tag]
@ -199,8 +202,77 @@
(children element)))))) (children element))))))
(defn all-properties (defn insertable?
"Return all properties of this entity (including key properties)." "Return `true` it the value of this `property` may be set from user-supplied data."
[entity] [property]
(descendants-with-tag entity :property)) (and
(= (:tag property) :property)
(not (= (:distinct (:attrs property)) "system"))))
(defmacro all-properties
"Return all properties of this `entity` (including key properties)."
[entity]
`(descendants-with-tag ~entity :property))
(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 key-names [entity]
(remove
nil?
(map
#(:name (:attrs %))
(key-properties entity))))
(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))

10
test/adl/utils_test.clj Normal file
View file

@ -0,0 +1,10 @@
(ns adl.utils-test
(:require [clojure.string :as s]
[clojure.test :refer :all]
[adl.utils :refer :all]))
(deftest singularise-tests
(testing "Singularise"
(is (= "address" (singularise "addresses")))
(is (= "address" (singularise "address")))
(is (= "expertise" (singularise "expertise")))))