Search code examples
clojurespecifications

clojure spec fdef not working - while passing recursive definition


Here is my clojure spec written for hiccup like syntax.

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


(s/def ::tag (s/and
              keyword?
              #(re-matches #":[a-z]+([0-9]+)?"
                           (str  %))))

(s/def ::org-content (s/cat
                      :tag ::tag
                      :content (s/+ (s/or
                                     :str string?
                                     :content ::org-content
                                     ))))

I wrote a simple function spec here -

(s/fdef org-headers-h3
        :args (s/cat :contact ::org-content)
        :ret keyword?)

(defn org-headers-h3 [doc]
  (first doc))

(st/instrument `org-headers-h3)

(org-headers-h3 [:div [:h1 "d"]])

There leads the following error -

*Call to #'modcss2.parser/org-headers-h3 did not conform to spec:
   In: [0] val: [:div [:h1 "d"]] fails spec: :modcss2.parser/tag at:
   [:args :contact :tag] predicate: keyword?  :clojure.spec.alpha/spec
   #object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x6613f384
   "clojure.spec.alpha$regex_spec_impl$reify__1200@6613f384"]
   :clojure.spec.alpha/value ([:div [:h1 "d"]])
   :clojure.spec.alpha/args ([:div [:h1 "d"]])
   :clojure.spec.alpha/failure :instrument*

I seem to me, I am getting error for passing wrong type of argument. but I am getting true for following statement.

(s/valid? ::org-content [:div [:h1 "d"]]) => true

Solution

  • One s/cat inside another doesn't mean they're nested, they just concatenate. This goes for all the regex specs.

    (s/conform (s/cat :foo (s/cat :bar int?)) [1]) ;=> {:foo {:bar 1}}
    
    (s/conform (s/cat :foo (s/cat :bar int?)) [[1]]) ;=> ::s/invalid
    

    Your :args spec expands to something like: (s/cat :contact (s/cat :tag ::tag ,,,))

    To nest them you can use s/spec.

    (s/conform (s/cat :foo (s/spec (s/cat :bar int?))) [[1]]) ;=> {:foo {:bar 1}}
    

    All regex specs concatenate like that. In this case it makes sense to use s/and to both check that it's a vector and make it a non-regex spec so it nests normally.

    (s/conform (s/cat :foo (s/and vector? (s/cat :bar int?))) [[1]]) ;=> {:foo {:bar 1}}
    

    So, to fix your problem:

    (s/def ::org-content
      (s/and vector?
             (s/cat
              :tag ::tag
              :content (s/+ (s/or
                             :str string?
                             :content ::org-content)))))