Search code examples
clojurescriptclojure.spec

Problem with Clojure Spec about simple parameter matching


I'm struggling with Clojure(script) spec. I slightly found out what part causes problem but I can't solve it.

(defn filter-ids
  [[items fields] _]
  (let [ids
        (for [item items
              field-tags (vals fields)
              :let [item-tags (-> item second :tags)
                    item-id (first item)]
              :when (and
                     (seq field-tags)
                     (empty? (set/difference field-tags item-tags)))]
          item-id)]
    (into #{} ids)))

Above code is what I tried to define spec. (fdef)

And I defined spec.

(spec/def :common/id (spec/and
                      keyword?
                      #(-> %1 name js/parseInt nat-int?)))

(spec/def :common/label string?)

(spec/def :common/tags (spec/coll-of string? :kind set?))

(spec/def :common/item (spec/keys :req-un [:common/label :common/tags]))


(spec/fdef filter-ids
  :args (spec/cat
         :useful (spec/cat
                  :items (spec/map-of :common/id :common/item)
                  :fields (spec/map-of :common/id :common/tags))
         :useless any?)
  :ret (spec/coll-of :common/id :kind set?))

And when I run it with instrument, error occurs.

(stest/instrument `filter-ids)


(filter-ids [{:0 {:label "task0" :tags #{"one" "two"}}}
             {:0 #{"three"}, :1 #{"one"}}]
            nil)


; Execution error - invalid arguments to taggy.states.subs/filter-ids at (<cljs repl>:1).
[{:0 {:label "task0", :tags #{"two" "one"}}} {:0 #{"three"}, :1 #{"one"}}] - failed: map? at: [:useful :items]

It seems like spec think first argument needs to be map, which is what I'm not intended to.

When I do like below, it doesn't complaining about map?. (although still a error because it's not valid at all)

(filter-ids {{:0 {:label "task0" :tags #{"one" "two"}}} 1
             {:0 #{"three"}, :1 #{"one"}} 2}
            nil)

I'm a newbie and really need some help to move on.

Thanks.


Solution

  • spec/cat is a "sequence regex" and it "unrolls" if you nest it inside another spec/cat.

    You can either wrap the inner spec/cat call in a spec/spec call, which prevents that unrolling, or you can switch to spec/tuple (and remove the :items and :fields labels):

    (spec/fdef filter-ids
      :args (spec/cat
             :useful (spec/spec (spec/cat
                                  :items (spec/map-of :common/id :common/item)
                                  :fields (spec/map-of :common/id :common/tags)))
             :useless any?)
      :ret (spec/coll-of :common/id :kind set?))
    ;; or
    (spec/fdef filter-ids
      :args (spec/cat
             :useful (spec/tuple
                      (spec/map-of :common/id :common/item)
                      (spec/map-of :common/id :common/tags))
             :useless any?)
      :ret (spec/coll-of :common/id :kind set?))
    

    Both of those will work. Which you choose may depend on what information you want in your error messages (I think the former provides more context when you get something wrong because of the :items and :fields labels).