Say we have a function get-ints
with one positional argument, the number of ints the caller wants, and two named arguments :max
and :min
like:
; Ignore that the implementation of the function is incorrect.
(defn get-ints [nr & {:keys [max min] :or {max 10 min 0}}]
(take nr (repeatedly #(int (+ (* (rand) (- max min -1)) min)))))
(get-ints 5) ; => (8 4 10 5 5)
(get-ints 5 :max 100) ; => (78 43 32 66 6)
(get-ints 5 :min 5) ; => (10 5 9 9 9)
(get-ints 5 :min 5 :max 6) ; => (5 5 6 6 5)
How does one write a Plumatic Schema for the argument list of get-ints
, a list of one, three or five items where the first one is always a number and the following items are always pairs of a keyword and an associated value.
With Clojure Spec I'd express this as:
(require '[clojure.spec :as spec])
(spec/cat :nr pos-int? :args (spec/keys* :opt-un [::min ::max]))
Along with the separate definitions of valid values held by ::min
and ::max
.
Based on the answer I got from the Plumatic mailing list [0] [1] I sat down and wrote my own conformer outside of the schema language itself:
(defn key-val-seq?
([kv-seq]
(and (even? (count kv-seq))
(every? keyword? (take-nth 2 kv-seq))))
([kv-seq validation-map]
(and (key-val-seq? kv-seq)
(every? nil? (for [[k v] (partition 2 kv-seq)]
(if-let [schema (get validation-map k)]
(schema/check schema v)
:schema/invalid))))))
(def get-int-args
(schema/constrained
[schema/Any]
#(and (integer? (first %))
(key-val-seq? (rest %) {:max schema/Int :min schema/Int}))))
(schema/validate get-int-args '()) ; Exception: Value does not match schema...
(schema/validate get-int-args '(5)) ; => (5)
(schema/validate get-int-args [5 :max 10]) ; => [5 :max 10]
(schema/validate get-int-args [5 :max 10 :min 1]); => [5 :max 10 :min 1]
(schema/validate get-int-args [5 :max 10 :b 1]) ; Exception: Value does not match schema...