Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save christianromney/f49775eafad2791d4bc719e7ddfce633 to your computer and use it in GitHub Desktop.

Select an option

Save christianromney/f49775eafad2791d4bc719e7ddfce633 to your computer and use it in GitHub Desktop.
Conditional map generation with clojure.test.check.generators + predicate-based validation with Plumatic Schema

Conditional Generators & Predicate Schemas in Clojure

1. clojure.test.check.generators — conditional map generation

Use gen/let to generate :type first, then branch the rest of the map on it — this expresses a cross-key invariant (e.g. :info requires [:button :dismiss] to be present and non-nil, :critical allows it to be absent).

(require '[clojure.test.check.generators :as gen])

(def alert-widget-gen
  (gen/let [type (gen/elements [:info :critical])
            button (if (= type :info)
                     (gen/fmap (fn [s] {:dismiss s}) gen/string-alphanumeric)
                     (gen/one-of [(gen/return {})
                                  (gen/fmap (fn [s] {:dismiss s}) gen/string-alphanumeric)]))]
    {:type type :button button}))

(gen/sample alert-widget-gen 5)

gen/let is sugar over gen/bind/gen/fmap and lets later bindings depend on earlier ones (monadic bind), which is what makes the conditional branching possible.

API docs: https://clojure.github.io/test.check/clojure.test.check.generators.html

2. Plumatic Schema — predicate-based validation

Yes — built in via s/pred and s/constrained.

(require '[schema.core :as s])

;; s/pred — wrap a bare predicate as a schema
(def NonEmptyString (s/pred #(and (string? %) (seq %)) 'non-empty-string))
(s/validate NonEmptyString "X")   ;; passes
(s/validate NonEmptyString "")    ;; throws

;; s/constrained — attach a predicate as an additional invariant on an existing schema
(def PositiveInt (s/constrained s/Int pos? 'pos?))
(s/validate PositiveInt 5)   ;; passes
(s/validate PositiveInt -5)  ;; throws

For a cross-key invariant like the alert-widget example above, wrap a map schema with s/constrained:

(def AlertWidget
  (s/constrained
    {:type (s/enum :info :critical)
     :button {(s/optional-key :dismiss) s/Str}}
    (fn [{:keys [type button]}]
      (or (not= type :info) (some? (:dismiss button))))
    'info-requires-dismiss))

API docs: https://cljdoc.org/d/prismatic/schema/CURRENT/api/schema.core

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment