Clojure.Spec: error when using spec/cat to specificate maps

93 views Asked by At

I have this spec for a customer (using spec/cat instead of spec/keys for readability reasons):

(ns thrift-store.entities.customer
  (:require [clojure.spec.alpha :as s]
            [thrift-store.entities.address :refer :all]))

(s/def ::customer
  (s/cat
   :id uuid?
   :name string?
   :document string?
   :email (s/nilable string?)
   :phone-number (s/nilable string?)
   :address (s/nilable #(s/map-of ::address %))))

(defn generate-test-customer [name document email phone-number address]
  (let [customer {:id (java.util.UUID/randomUUID)
                  :name name
                  :document document
                  :email email
                  :phone-number phone-number
                  :address address}]
     (if (s/valid? ::customer customer)
       customer
       (throw (ex-info "Test customer does not conform to the specification" 
                       {:customer customer} (s/explain ::customer customer))))))

(def test-customer (generate-test-customer "Arcles" "86007877000" nil nil test-address))

I get the exception: 'Test customer does not conform to the specification', but isn't the generated customer correct?

Now all the map building functions of my project are no longer working, and are throwing the exceptions, just because I changed spec/keys and countless individual specs to spec/cat and direct specs.

https://github.com/guilhermedjr/thrift-store/commit/a1f625a1aceb2586fdcd3a527cb39089af2bf3ec

2

There are 2 answers

4
Martin Půda On BEST ANSWER

Look into docs to see definition and examples for cat:

Takes key+pred pairs, e.g. (s/cat :e even? :o odd?)

Returns a regex op that matches (all) values in sequence, returning a map containing the keys of each pred and the corresponding value.

(let [spec (s/cat :e even? :o odd?)]
  [(s/conform spec [22 11])
   (s/conform spec [22])
   (s/conform spec [22 11 22])
   (s/conform spec 22)
   (s/conform spec "22")])
;; => [{:e 22, :o 11}
;;     :clojure.spec.alpha/invalid
;;     :clojure.spec.alpha/invalid
;;     :clojure.spec.alpha/invalid
;;     :clojure.spec.alpha/invalid]

As you can see, cat describes a sequential data structure (vector, list, sequence...), keys describes hash-map- they are entirely different and it has nothing to do with readability. When you use cat, the valid customer could look like this:

(defn generate-test-customer [name document email phone-number address]
  (let [customer [(UUID/randomUUID)
                  name
                  document
                  email
                  phone-number
                  address]]
    (if (s/valid? ::customer customer)
      customer
      (throw (ex-info "Test customer does not conform to the specification"
                      {:customer customer} (s/explain ::customer customer))))))

By the way- since version 1.11, Clojure has random-uuid.

EDIT: Are you sure you want to use (s/nilable #(s/map-of ::address %))?

(s/valid? (s/nilable #(s/map-of ::address %))
          3)
=> true

(s/valid? (s/nilable #(s/map-of ::address %))
          "test")
=> true

Proper use would be something like (s/nilable (s/map-of keyword? (s/nilable string?)))... but I think you just want to write :address (s/nilable ::address).

0
Shmuel Greenberger On

What you're describing in generate-test-customer is the spec/conform result. As @martin-puda points out, the spec requires a vector. However, you can convert the produced map in your test to a vector conforming to the spec using spec.unform, like so:

(if (s/valid? ::customer (s/unform ::customer customer))
       customer
       (throw (ex-info "Test customer does not conform to the specification" 
                       {:customer customer}
(s/explain ::customer (s/unform ::customer customer)))))))

With that said, I admit that this probably isn't what you're looking for, but this will provide clarification as to where maps are connected with spec.cat.