Search code examples
clojureplumatic-schema

Plumatic Schema for keyword arguments


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.


Solution

  • 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...