Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure Spec for a map with non-keyword keys

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?

like image 557
Rovanion Avatar asked Apr 17 '17 14:04

Rovanion


1 Answers

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.

like image 196
Alex Miller Avatar answered Sep 23 '22 20:09

Alex Miller