Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by

Hi there, I am here to report what I think is my second quirk in a week with spec instrumentation.

I apologize if it has been already reported as bug but basically I cannot understand/expected the last call to trigger an error - the ::id-map spec does not require the check on ::other after all.

(ns repro-spec.test
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.test.alpha :as st]))

(s/def ::impl any?)
(s/def ::id string?)
(s/def ::other number?)

(s/def ::id-map (s/keys :req [::id]))

(s/fdef delete-impl
  :args (s/cat :impl ::impl
               :id-map ::id-map))

(defn delete-impl [_ id-map]
  (println (str "This deletes " (::id id-map)))


(delete-impl {} {::id "test"})
;; prints "This deletes test"

(delete-impl {} {::id "test" ::other "ignore me"})
;; Execution error - invalid arguments to repro-spec.test/delete-impl at (REPL:27).
;; "ignore me" - failed: number? at: [:id-map :repro-spec.test/other] spec: :repro-spec.test/other

I am doing something wrong?

1 Answer

0 votes

This is by design and is described in the documentation: https://clojure.org/guides/spec#_entity_maps

When conformance is checked on a map, it does two things - checking that the required attributes are included, and checking that every registered key has a conforming value. We’ll see later where optional attributes can be useful. Also note that ALL attributes are checked via keys, not just those listed in the :req and :opt keys. Thus a bare (s/keys) is valid and will check all attributes of a map without checking which keys are required or optional.

(my emphasis)

Wow indeed it is there! TIL :D

Do you know by any chance the rationale around it? It seems `s/keys` would allow "restricting" the scope of what you want to check - as in, only check these keys please - but then the check is performed on all of them?
I suspect Alex is better placed to comment on that but my understanding is that qualified Spec names are intended to give global meaning to a global name while s/keys is for identifying whether specific keys are required or not (:opt is essentially for documentation purposes, to say "I might use these keys too"). The requiredness is orthogonal to the global meaning of the names.

Note that this is not true for :req-un / :opt-un since those keys are unqualified in the map and do not have global meaning -- only what is assigned by the qualification of the name within s/keys (to the associated qualified Spec names).
> The requiredness is orthogonal to the global meaning of the names.

Thank you for the explanation the above is what I was missing. Global things do make things more difficult to reason about in isolation (imho, maybe obvious). I can see now why things like `select` in `spec2` have popped up.
You might find reading the spec rationale and guide to be helpful in filling in some of these questions:

* https://clojure.org/about/spec
* https://clojure.org/guides/spec
The first link is helpful thank you.