This commit is contained in:
Simon Brooke 2018-07-09 21:57:56 +01:00
parent 7d62976880
commit 238bbf1187
3 changed files with 229 additions and 146 deletions

3
.gitignore vendored
View file

@ -9,6 +9,7 @@ pom.xml.asc
/.nrepl-port
.hgignore
.hg/
.idea
resources/auto/
@ -17,3 +18,5 @@ generated/resources/sql/
generated/resources/templates/auto/
generated/src/clj/youyesyet/routes/
*.iml

View file

@ -39,7 +39,7 @@
{:tag :div
:attrs {:class "big-link-container"}
:content
[{:tag :a :attrs {:href url :class "big-link"}
[{:tag :a :attrs {:href (str "{{servlet-context}}/" url) :class "big-link"}
:content (if
(vector? content)
content
@ -51,7 +51,7 @@
{:tag :div
:attrs {:class "back-link-container"}
:content
[{:tag :a :attrs {:href url}
[{:tag :a :attrs {:href (str "{{servlet-context}}/" url)}
:content (if
(vector? content)
content
@ -60,18 +60,20 @@
(defn emit-content
([content]
(cond
(nil? content)
nil
(string? content)
content
(and (map? content) (:tag content))
(with-out-str
(x/emit-element content))
(seq? content)
(map emit-content content)
true
(str "<!-- don't know what to do with '" content "' -->")))
(try
(cond
(nil? content)
nil
(string? content)
content
(and (map? content) (:tag content))
(with-out-str
(x/emit-element content))
(seq? content)
(map emit-content content)
true
(str "<!-- don't know what to do with '" content "' -->"))
(catch Exception _ (str "<!-- failed while trying to emit '" content " -->"))))
([filename application k]
(emit-content filename nil nil application k))
([filename spec entity application k]
@ -94,15 +96,6 @@
content))
"{% endblock %}"))))))
;; {:tag :div, :content
;; [{:tag :div, :attrs {:class big-link-container}, :content
;; [{:tag :a, :attrs {:id next-selector, :role button, :class big-link},
;; :content [Next]}]}
;; [{% ifmemberof admin %}
;; {:tag :div, :attrs {:class big-link-container}, :content
;; [{:tag :a, :attrs {:href form-electors-Elector, :class big-link}, :content [Add a new Elector]}]}
;; {% endifmemberof %}]]}
(defn file-header
"Generate a header for a template file with this `filename` for this `spec`
@ -155,7 +148,8 @@
(= (:tag %) :prompt)
(= (:locale :attrs %) *locale*))))
(:name (:attrs field-or-property))
(:property (:attrs field-or-property))))))
(:property (:attrs field-or-property))
"Missing prompt"))))
(defn csrf-widget
@ -165,15 +159,15 @@
(defn compose-if-member-of-tag
[writable? & elts]
[privilege & elts]
(let
[all-permissions (distinct (apply find-permissions elts))
permissions (map
s/lower-case
(if
writable?
(writable-by all-permissions)
(visible-to all-permissions)))]
(case privilege
:writeable (writeable-by all-permissions)
:editable (writeable-by all-permissions true)
:readable (visible-to all-permissions)))]
(s/join
" "
(flatten
@ -184,12 +178,12 @@
(defn wrap-in-if-member-of
"Wrap this `content` in an if-member-of tag; if `writable?` is true,
allow those groups by whom it is writable, else those by whom it is
"Wrap this `content` in an if-member-of tag; if `writeable?` is true,
allow those groups by whom it is writeable, else those by whom it is
readable. `context` should be a sequence of adl elements from which
permissions may be obtained."
[content writable? & context]
[(apply compose-if-member-of-tag (cons writable? context))
[content privilege & context]
[(apply compose-if-member-of-tag (cons privilege context))
content
"{% endifmemberof %}"])
@ -211,7 +205,7 @@
:class "action-safe"
:type "submit"
:value (str "Save!")}}]}
true
:editable
entity
application))
@ -233,7 +227,7 @@
:class "action-dangerous"
:type "submit"
:value (str "Delete!")}}]}
true
:editable
entity
application))
@ -275,9 +269,8 @@
(:type (:attrs typedef))
(:type (:attrs property)))]
(if
(= (-> property :attrs :distinct) "system")
"hidden" ;; <- this is slightly wrong. There are some circumstances in which
;; system-distinct properties might be user-editable
(and (= (-> property :attrs :distinct) "system") (= (-> property :attrs :immutable) "true"))
"hidden"
(case t
("integer" "real" "money") "number"
("uploadable" "image") "file"
@ -286,8 +279,8 @@
"date" "date"
"time" "time"
"text" "text-area"
"string" ;; default
)))))
;; default
"string")))))
(defn select-widget
@ -337,30 +330,49 @@
:content (apply vector (get-options property form entity application))}))))}))
(defn compose-readable-or-not-authorised
[p f e a w]
(list
(compose-if-member-of-tag :readable p e a)
{:tag :span
:attrs {:id w
:name w
:class "pseudo-widget disabled"}
:content [(str "{{record." w "}}")]}
"{% else %}"
{:tag :span
:attrs {:id w
:name w
:class "pseudo-widget not-authorised"}
:content [(str "You are not permitted to view " w " of " (:name (:attrs e)))]}
"{% endifmemberof %}"
))
(defn compose-widget-para
[p f e a w content]
{:tag :p
:attrs {:class "widget"}
:content [{:tag :label
:attrs {:for w}
:content [(prompt p f e a)]}
(compose-if-member-of-tag true p e a)
content
"{% else %}"
(compose-if-member-of-tag false p e a)
{:tag :span
:attrs {:id w
:name w
:class "pseudo-widget disabled"}
:content [(str "{{record." w "}}")]}
"{% else %}"
{:tag :span
:attrs {:id w
:name w
:class "pseudo-widget not-authorised"}
:content [(str "You are not permitted to view " w " of " (:name (:attrs e)))]}
"{% endifmemberof %}"
"{% endifmemberof %}"]})
:content (apply
vector
(flatten
(list
{:tag :label
:attrs {:for w}
:content [(prompt p f e a)]}
(str "{% if {{record." (-> p :attrs :name) "}} %}")
(compose-if-member-of-tag :editable p e a)
content
"{% else %}"
(compose-readable-or-not-authorised p f e a w)
"{% endifmemberof %}"
"{% else %}"
(compose-if-member-of-tag :writeable p e a)
content
"{% else %}"
(compose-readable-or-not-authorised p f e a w)
"{% endifmemberof %}"
"{% endif %}")))})
(defn widget
@ -375,58 +387,62 @@
property (if
(= (:tag field-or-property) :property)
field-or-property
(child-with-tag entity
:property
#(= (:name (:attrs %))
(:property (:attrs field-or-property)))))
(first
(filter
#(= (:name (:attrs %))
(:property (:attrs field-or-property)))
(descendants-with-tag entity
:property))))
permissions (find-permissions field-or-property property form entity application)
typedef (typedef property application)
w-type (widget-type property application typedef)
visible-to (visible-to permissions)
;; if the form isn't actually a form, no widget is writable.
writable-by (if (= (:tag form) :form) (writable-by permissions))]
(case w-type
"hidden"
{:tag :input
:attrs {:id widget-name
:name widget-name
:type "hidden"
:value (str "{{record." widget-name "}}")}}
"select"
(compose-widget-para field-or-property form entity application widget-name
(select-widget property form entity application))
"text-area"
(compose-widget-para
field-or-property form entity application widget-name
{:tag :textarea
:attrs {:rows "8" :cols "60" :id widget-name :name widget-name}
:content [(str "{{record." widget-name "}}")]})
;; all others
(compose-widget-para
field-or-property form entity application widget-name
;; if the form isn't actually a form, no widget is writeable.
writeable-by (if (= (:tag form) :form) (writeable-by permissions))]
(if
property
(case w-type
"hidden"
{:tag :input
:attrs (merge
{:id widget-name
:name widget-name
:type w-type
: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
(:minimum (:attrs typedef))
{:min (:minimum (:attrs typedef))})
(if
(:maximum (:attrs typedef))
{:max (:maximum (:attrs typedef))}))}))))
:attrs {:id widget-name
:name widget-name
:type "hidden"
:value (str "{{record." widget-name "}}")}}
"select"
(compose-widget-para property form entity application widget-name
(select-widget property form entity application))
"text-area"
(compose-widget-para
property form entity application widget-name
{:tag :textarea
:attrs {:rows "8" :cols "60" :id widget-name :name widget-name}
:content [(str "{{record." widget-name "}}")]})
;; all others
(compose-widget-para
property form entity application widget-name
{:tag :input
:attrs (merge
{:id widget-name
:name widget-name
:type w-type
: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
(:minimum (:attrs typedef))
{:min (:minimum (:attrs typedef))})
(if
(:maximum (:attrs typedef))
{:max (:maximum (:attrs typedef))}))})))))
(defn embed-script-fragment
@ -446,14 +462,16 @@
(defn compose-form-content
[form entity application]
{:content
{:tag :div
:attrs {:id "content" :class "edit"}
:content
[{:tag :form
:attrs {:action (str "{{servlet-context}}/" (editor-name entity application))
:method "POST"}
:content (flatten
{:content
{:tag :div
:attrs {:id "content" :class "edit"}
:content
[{:tag :form
:attrs {:action (str "{{servlet-context}}/" (editor-name entity application))
:method "POST"}
:content (apply
vector
(flatten
(list
(csrf-widget)
(map
@ -469,7 +487,7 @@
(= (:distict (:attrs property)) :system))
(children-with-tag form :field)))
(save-widget form entity application)
(delete-widget form entity application)))}]}})
(delete-widget form entity application))))}]}})
(defn compose-form-extra-head
@ -587,6 +605,7 @@
(defn edit-link
[entity application parameters]
(str
"{{servlet-context}}/"
(editor-name entity application)
"?"
(s/join
@ -677,7 +696,7 @@
:content ["Next"]}]}
(wrap-in-if-member-of
(big-link (str "Add a new " (pretty-name entity)) (editor-name entity application))
true
:writeable
entity
application)))))}
:content
@ -761,9 +780,9 @@
{:tag :dt
:content
[{:tag :a
:attrs {:href (path-part :list entity application)}
:attrs {:href (str "{{servlet-context}}/" (path-part :list entity application))}
:content [(pretty-name entity)]}]}
false
:readable
entity
application))
@ -781,37 +800,37 @@
:tag :p
:content (:content d)))
(children-with-tag entity :documentation)))}
false
:readable
entity
application))
(defn application-to-template
[application]
(let
[first-class-entities
(sort-by
#(:name (:attrs %))
(filter
#(children-with-tag % :list)
(children-with-tag application :entity)))]
{:application-index
{:content
{:tag :dl
:attrs {:class "index"}
:content
(apply
vector
(remove
nil?
(flatten
(interleave
(map
#(emit-entity-dt % application)
first-class-entities)
(map
#(emit-entity-dd % application)
first-class-entities)))))}}}))
(defn application-to-template
[application]
(let
[first-class-entities
(sort-by
#(:name (:attrs %))
(filter
#(children-with-tag % :list)
(children-with-tag application :entity)))]
{:application-index
{:content
{:tag :dl
:attrs {:class "index"}
:content
(apply
vector
(remove
nil?
(flatten
(interleave
(map
#(emit-entity-dt % application)
first-class-entities)
(map
#(emit-entity-dd % application)
first-class-entities)))))}}}))
(defn write-template-file

61
src/adl/to_swagger.clj Normal file
View file

@ -0,0 +1,61 @@
(ns ^{:doc "Application Description Language: generate swagger routes."
:author "Simon Brooke"}
adl.to-swagger
(:require [adl-support.utils :refer :all]
[adl.to-hugsql-queries :refer [queries]]
[clj-time.core :as t]
[clj-time.format :as f]
[clojure.java.io :refer [file make-parents writer]]
[clojure.pprint :refer [pprint]]
[clojure.string :as s]
[clojure.xml :as x]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; adl.to-swagger: generate swagger routes.
;;;;
;;;; 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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; The overall structure of this has quite closely to follow the structure of
;;; to-hugsql-queries, because essentially we need one JSON entry point to wrap
;;; each query.
(defn file-header [application]
(list
'ns
(symbol (str (safe-name (:name (:attrs application))) ".routes.auto-api"))
(str "API routes for " (:name (:attrs application))
" auto-generated by [Application Description Language framework](https://github.com/simon-brooke/adl) at "
(f/unparse (f/formatters :basic-date-time) (t/now)))
(list
:require
'[adl-support.core :as support]
'[clj-http.client :as client]
'[clojure.tools.logging :as log]
'[compojure.api.sweet :refer :all]
'[hugsql.core :as hugsql]
'[ring.util.http-response :refer :all]
'[noir.response :as nresponse]
'[noir.util.route :as route]
'[ring.util.http-response :as response]
'[schema.core :as s]
(vector (symbol (str (safe-name (:name (:attrs application))) ".db.core")) :as 'db))))