adl-support/src/adl_support/utils.clj
2018-07-20 14:16:05 +01:00

559 lines
15 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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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.UTF-8")
(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
"_" (cons "ln" (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 writeable-by
"Return a list of names of groups to which are granted write access,
given these `permissions`, else nil.
TODO: TOTHINKABOUT: properties are also writeable by `insert` and `noedit`, but only if the
current value is nil."
([permissions]
(writeable-by permissions true))
([permissions has-value?]
(let
[privileges (if has-value? #{"edit" "all"} #{"edit" "all" "insert" "noedit"})]
(permission-groups permissions #(privileges (: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]
(if
(string? s)
(s/join
" "
(map
#(apply str (cons (Character/toUpperCase (first %)) (rest %)))
(s/split s #"[ \t\r\n]+")))
s))
(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 element."
([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 property-for-field
"Return the property within this `entity` which matches this `field`."
[field entity]
(child-with-tag
entity
:property
#(=
(-> field :attrs :property)
(-> % :attrs :name))))
(defn prompt
"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
binding of `*locale*`. TODO: something more sophisticated about i18n"
[field-or-property form entity application]
(let [property (case (:tag field-or-property)
:property field-or-property
:field (property-for-field field-or-property entity)
nil)]
(capitalise
(or
(:prompt
(:attrs
(child-with-tag
field-or-property
:prompt
#(= (:locale (:attrs %)) *locale*))))
(:prompt
(:attrs
(child-with-tag
property
:prompt
#(= (:locale (:attrs %)) *locale*))))
(:name (:attrs property))
(:property (:attrs field-or-property))
"Missing prompt"))))
(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`.
If `predicate` is specified, return only those also satisfying this `predicate`."
([element tag]
(flatten
(remove
empty?
(cons
(children element #(= (:tag %) tag))
(map
#(descendants-with-tag % tag)
(children element))))))
([element tag predicate]
(filter
predicate
(descendants-with-tag element tag))))
(defn descendant-with-tag
"Return the first descendant of this `element`, recursively, which has this `tag`.
If `predicate` is specified, return the first also satisfying this `predicate`."
([element tag]
(first (descendants-with-tag element tag)))
([element tag predicate]
(first (descendants-with-tag element tag predicate))))
(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)))))
([entity as-keywords?]
(let [names (key-names entity)]
(if
as-keywords?
(set (map keyword names))
names))))
(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)))