Lets look at the real-world example of Leiningen project maps :global-vars
:
;; Sets the values of global vars within Clojure. This example
;; disables all pre- and post-conditions and emits warnings on
;; reflective calls. See the Clojure documentation for the list of
;; valid global variables to set (and their meaningful values).
:global-vars {*warn-on-reflection* true
*assert* false}
It allows the user of leiningen to redefine the default values of Clojures global variables for the scope of their project.
Now, if the keys of this map consisted of keywords we'd use clojure.spec/keys
to first specify which keys could be part of the map and then separately define the values expected under those keys. But since clojure.spec/keys
silently ignores non-keywords in :req
and :req-un
and throws an exception for :opt
and :opt-un
(as of alpha15) we'll have to work around that in some way.
We can get the type of most of these global variables through
(for [[sym varr] (ns-publics 'clojure.core)
:when (re-matches #"\*.+\*" (name sym))]
[varr (type @varr)])
=>
[*print-namespace-maps* java.lang.Boolean]
[*source-path* java.lang.String]
[*command-line-args* clojure.lang.ArraySeq]
[*read-eval* java.lang.Boolean]
[*verbose-defrecords* java.lang.Boolean]
[*print-level* nil]
[*suppress-read* nil]
[*print-length* nil]
[*file* java.lang.String]
[*use-context-classloader* java.lang.Boolean]
[*err* java.io.PrintWriter]
[*default-data-reader-fn* nil]
[*allow-unresolved-vars* java.lang.Boolean]
[*print-meta* java.lang.Boolean]
[*compile-files* java.lang.Boolean]
[*math-context* nil]
[*data-readers* clojure.lang.PersistentArrayMap]
[*clojure-version* clojure.lang.PersistentArrayMap]
[*unchecked-math* java.lang.Boolean]
[*out* java.io.PrintWriter]
[*warn-on-reflection* nil]
[*compile-path* java.lang.String]
[*in* clojure.lang.LineNumberingPushbackReader]
[*ns* clojure.lang.Namespace]
[*assert* java.lang.Boolean]
[*print-readably* java.lang.Boolean]
[*flush-on-newline* java.lang.Boolean]
[*agent* nil]
[*fn-loader* nil]
[*compiler-options* nil]
[*print-dup* java.lang.Boolean]
and the rest can we fill in by reading the docs. But my question to you is: How do we write a spec which makes sure that if the map contains the key '*assert*
it will only hold boolean values?
FYI, there are no plans right now to support non-keyword keys in s/keys
.
There are several ways to do it (one would be to do something like clojure.walk/keywordize-keys
before validation or in a leading conformer, and then use s/keys
).
Another path is to treat a map as a collection of map entry tuples (some macros could clean this up considerably):
(defn warn-on-reflection? [s] #(= % '*warn-on-reflection*))
(s/def ::warn-on-reflection (s/tuple warn-on-reflection? boolean?))
(defn assert? [s] #(= % '*assert*))
(s/def ::assert (s/tuple assert? boolean?))
(s/def ::global-vars
(s/coll-of (s/or :wor ::warn-on-reflection, :assert ::assert) :kind map?))
And finally s/multi-spec
might be interesting to try instead of the s/or
above - that would make this an open spec that could be added to later. In some way or another, it's probably useful to make this spec open (so also accept (s/tuple any? any?)
- since there may be new things added in the future or not listed here (for example, there are dynamic vars from other namespaces too).
Also, be careful of some of the attribute specs on these. For example, *unchecked-math*
is treated as a logically true value and in particular takes the special value :warn-on-boxed
which is logical true, but also triggers extra behavior.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With