495 lines
13 KiB
Clojure
495 lines
13 KiB
Clojure
(ns ^{:doc "Application Description Language support library - utility functions."
|
|
:author "Simon Brooke"}
|
|
adl-support.utils
|
|
(:require [clojure.math.numeric-tower :refer [expt]]
|
|
[clojure.pprint :as p]
|
|
[clojure.string :as s]
|
|
[clojure.tools.logging :as log]
|
|
[clojure.xml :as x]))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;;;
|
|
;;;; adl-support.utils: utility functions.
|
|
;;;;
|
|
;;;; This program is free software; you can redistribute it and/or
|
|
;;;; modify it under the terms of the MIT-style licence provided; see LICENSE.
|
|
;;;;
|
|
;;;; 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
|
|
;;;; License for more details.
|
|
;;;;
|
|
;;;; Copyright (C) 2018 Simon Brooke
|
|
;;;;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; TODO: this really ought to be split into several namespaces
|
|
|
|
(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."
|
|
"generated/")
|
|
|
|
(def ^:dynamic *verbosity*
|
|
"The verbosity of output from the generator."
|
|
0)
|
|
|
|
|
|
(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 #" "))))
|
|
([text]
|
|
(wrap-lines 76 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`. However, there
|
|
may be different links between the same two tables with different semantics; if
|
|
`property` is specified, and if more than one property in `e1` links to `e2`, generate
|
|
a more specific link name."
|
|
([e1 e2]
|
|
(s/join
|
|
"_"
|
|
(cons
|
|
"ln"
|
|
(sort
|
|
(list
|
|
(:name (:attrs e1)) (:name (:attrs e2)))))))
|
|
([property e1 e2]
|
|
(if (count
|
|
(descendants
|
|
e1
|
|
#(and
|
|
(= (-> % :attrs :type) "link")
|
|
(=
|
|
(-> % :attrs :entity)
|
|
(-> property :attrs :entity)))))
|
|
(s/join
|
|
"_" (map #(:name (:attrs %)) (list property e1 e2)))
|
|
(link-table-name e1 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 children-with-tag
|
|
"Return all children of this `element` which have this `tag`;
|
|
if `element` is `nil`, return `nil`. If `predicate` is supplied,
|
|
return only those children with the specified `tag` which satisfy
|
|
the `predicate`."
|
|
([element tag]
|
|
(if
|
|
element
|
|
(children element #(= (:tag %) tag))))
|
|
([element tag predicate]
|
|
(filter
|
|
predicate
|
|
(children-with-tag element tag))))
|
|
|
|
|
|
(defn child-with-tag
|
|
"Return the first child of this `element` which has this `tag`;
|
|
if `element` is `nil`, return `nil`. If `predicate` is supplied,
|
|
return only the first child with the specified `tag` which satisfies
|
|
the `predicate`."
|
|
([element tag]
|
|
(first (children-with-tag element tag)))
|
|
([element tag predicate]
|
|
(first (children-with-tag element tag predicate))))
|
|
|
|
|
|
|
|
(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 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))))))
|
|
|
|
|
|
(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 find-permissions
|
|
"Return appropriate the permissions of the first of these `elements` which
|
|
has permissions."
|
|
[& elements]
|
|
(first
|
|
(remove
|
|
empty?
|
|
(map
|
|
#(children-with-tag % :permission)
|
|
elements))))
|
|
|
|
|
|
(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)))
|
|
|
|
|
|
(defn user-distinct-property-names
|
|
"Return, as a set, the names of properties which are user distinct"
|
|
[entity]
|
|
(set
|
|
(map
|
|
(fn [x] (-> x :attrs :name))
|
|
(user-distinct-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]
|
|
(set
|
|
(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))))
|
|
|
|
|
|
(defn volatility
|
|
"Return the cache ttl in seconds for records of this `entity`."
|
|
[entity]
|
|
(try
|
|
(let
|
|
[v (read-string (:volatility (:attrs entity)))]
|
|
(if
|
|
(zero? v)
|
|
0
|
|
(expt 10 v)))
|
|
(catch Exception _ 0)))
|