Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

clojure.spec/unform returning non-conforming values

I've been getting on quite well with clojure.spec for the most part. However, I came to a problem that I couldn't figure out when dealing with unform. Here's a loose spec for Hiccup to get us moving:

(require '[clojure.spec :as s])

(s/def ::hiccup
  (s/and
      vector?
      (s/cat
        :name       keyword?
        :attributes (s/? map?)
        :contents   (s/* ::contents))))

(s/def ::contents
  (s/or
    :element-seq (s/* ::hiccup)
    :element     ::hiccup
    :text        string?))

Now before we get carried away, let's see if it works with a small passing case.

(def example [:div])

(->> example
     (s/conform ::hiccup))

;;=> {:name :h1}

Works like a charm. But can we then undo our conformance?

(->> example
     (s/conform ::hiccup)
     (s/unform ::hiccup))

;;=> (:div)

Hmm, that should be a vector. Am I missing something? Let's see what spec has to say about this.

(->> example
     (s/conform ::hiccup)
     (s/unform ::hiccup)
     (s/explain ::hiccup))

;; val: (:div) fails spec: :user/hiccup predicate: vector?
;;=> nil

Indeed, it fails. So the question: How do I get this to work correctly?

like image 322
Hoagy Carmichael Avatar asked Nov 09 '22 10:11

Hoagy Carmichael


1 Answers

Late response here. I had not realised how old this question was but since I had already written a reply I might as well submit it.

Taking the spec that you provide for ::hiccup:

(s/def ::hiccup
  (s/and
      vector?
      (s/cat
        :name       keyword?
        :attributes (s/? map?)
        :contents   (s/* ::contents))))

The vector? spec within and will test the input data against that predicate. Unluckily as you have experienced, it doesn't unform to a vector.

Something that you can do to remedy this is to add an intermediate spec that will act as the identity when conforming and will act as desired when unforming. E.g.

(s/def ::hiccup                                                                                                                                                                                           
  (s/and vector?                                                                                                                                                                                          
         (s/conformer vec vec)                                                                                                                                                                            
         (s/cat                                                                                                                                                                                           
          :name       keyword?                                                                                                                                                                            
          :attributes (s/? map?)                                                                                                                                                                          
          :contents   (s/* ::contents)))) 

(s/unform ::hiccup (s/conform ::hiccup [:div]))

;;=> [:div]

(s/conformer vec vec) Will be a no-op for everything that satisfies vector? and using vec as the unforming function in the conformer will make sure that the result to unforming the whole and spec stays a vector.

I am new to spec myself and this was how I got it working as you intend. Bear in mind it might not be the way in which it was intended to be used by spec's designers.

like image 158
sui Avatar answered Nov 15 '22 06:11

sui