Search code examples
clojureclojure.spec

Clojure spec.alpha - How to (reference another argument) / (describe that argument collection should include values from another argument collection)


What I need: a spec for a function, that has two arguments:

  • a hash-map of keywords and strings.
  • a vector that may have strings or keywords but if it is a keyword it must exist inside hash-map (first argument)

(your answer doesn't have to cover all of this, mainly I need a way to tell that if it is a keyword it must exist in hash-map)

Here is what I have:

(this is an example to show that it is possible to access both arguments inside :args, I know that it doesn't test anything and always fails because nil is returned)

(ns my-example.core
  (:require
   [clojure.spec.alpha :as spec]))

(defn my-example [m v] nil)

(spec/fdef my-example
  :args (fn [[m v]] nil))

This fn kind of works (it is possible to create a function that would work how I want), But it isn't very descriptive and when it fails (given that there is (stest/instrument `my-example)) it just shows me body of function (like this: (fn [[m v]] nil)).

Is this the only way to solve my problem or there is a better way?

I also tryed to define a spec and use it inside :args :

(spec/def :my-example/my-check (fn [[m v]] nil))

(spec/fdef my-example
  :args :my-example/my-check)

But result is same.


Solution

  • In the spec for :args, you can specify any predicate you want. See the example provided at the spec guide for fdef. Given that example, here is a code fragment that mostly works for your case. I say "mostly" because the spec for the first map argument could be made stricter to note that it is a map of keywords to strings. The forms inside the comment form show some usage examples.

    (ns example
      (:require [clojure.spec.alpha :as s]
                [clojure.spec.test.alpha :as stest]))
    
    (defn my-example [m v] nil)
    
    (s/fdef my-example
      :args (s/and (s/cat :m map? :v vector?)
                   #(every? (fn [x] (or (string? x)
                                        (and (keyword? x)
                                             (contains? (:m %) x))))
                            (:v %)))
      :ret nil?)
    
    (comment
      (stest/instrument `my-example)
      (my-example {:a "a" :b "b"} ["foo" :a "bar" :b]) ; => nil
      (my-example {:a "a" :b "b"} ["foo" :a "bar" :c]) ; => spec exception
      (my-example {:a "a" :b "b"} ["foo" :a "bar" 2]) ; => spec exception
      )