My knowledge of Spec is still a bit superficial, but I think this is what :req-un and :opt-un are for?
You can definitely check unqualified keywords using spec. But namespaced qualified keys when used in maps get a special treatement, as their spec is globally enforced once registered. Consider the following example:
(require '[clojure.spec.alpha :as s])
(require '[clojure.spec.test.alpha :as stest])
(s/def :mc.cust/first-name string?)
(s/def :mc.contact/email string?)
(defn greet [customer]
(str "Hello, " (:mc.cust/first-name customer)))
(s/fdef greet
:args (s/cat :customer (s/keys :req [:mc.cust/first-name])))
(stest/instrument `greet)
(greet {:mc.cust/first-name "Bob" :mc.contact/email {}})
;; Throws, the following:
ExceptionInfo Call to #'user/greet did not conform to spec:
In: [0 :mc.contact/email] val: {} fails spec: :mc.contact/email at: [:args :customer :mc.contact/email] predicate: string?
clojure.core/ex-info (core.clj:4739)
Our spec’ed function only defined :mc.cust/first-name
, and really it is the only thing it cares about. But because :mc.cust/email
is a fully qualified keyword in the standard sense, spec enforces that if it is present in the map argument, it has to be a string.
I think it’s a really smart way of keeping dynamicity in the system. You do not have to provide for each function spec all the :opt-un [...]
for every possible piece of information that might flow through your system. But you still maintain a strong integrity of the value attached to your global names, and hopefully you get an error message closer to where you corrupted your data.
As I said before, maybe it’s just a matter of convincing spec to treat “all-terrain” keywords as fully-qualified to regain that property.