Search code examples
clojureclojure.spec

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


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


Solution

  • 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).