Combining s/and with s/or in Clojure spec - clojure

I want to write a spec for a map that either has the key :rule/children or has two keys - :condition/field and :condition/predicate. This is what I have tried:
(s/keys :req [(s/or :children :rule/children :condition (s/and :condition/field :condition/predicate))])
It results in the error message:
Caused by: java.lang.AssertionError: Assert failed: all keys must be namespace-qualified keywords
(every? (fn* [p1__1917#] (c/and (keyword? p1__1917#) (namespace p1__1917#))) (concat req-keys req-un-specs opt opt-un))
I know that for s/or each path must be named. Here there are two paths - this map can either have :children or be a :condition. It is a condition only if it has the two keys :condition/field and :condition/predicate.

In keys specs you can use plain or and and to do this:
(s/def ::map-spec
(s/keys :req [(or :rule/children (and :condition/field :condition/predicate))]))
(s/conform ::map-spec {:rule/children 1}) ;; valid
(s/conform ::map-spec {:condition/field 1}) ;; invalid
(s/conform ::map-spec {:condition/field 1 :condition/predicate 2}) ;; valid

Related

Clojure spec: Using a "spec" instead of a "pred" in "coll-of" actually works. Is this ok?

I'm trying out Clojure spec and calling it from a function :pre constraint.
My spec ::mustbe-seql-of-vec-of-2-int shall check whether the argument passed to the function is a sequential of anything from 0 to 4 vectors of exactly 2 integers:
(require '[clojure.spec.alpha :as s])
(s/def ::mustbe-seql-of-vec-of-2-int
(s/and
::justprint
(s/coll-of
::mustbe-vec-of-2-int
:kind sequential?
:min-count 0
:max-count 4)))
So the spec is composed of other specs, in particular ::justprint which does nothing except print the passed argument for debugging and coll-of to test the collection argument.
The doc for coll-of says:
Usage: (coll-of pred & opts)
Returns a spec for a collection of items satisfying pred. Unlike
'every', coll-of will exhaustively conform every value.
However, as first argument, I'm not using a pred (a function taking the argument-to-check and returning a truthy value) but another spec (a function taking the argument to check and returning I'm not sure what), in this case, the spec registered under ::mustbe-vec-of-2-int.
This works perfectly well.
Is this correct style and expected to work?
P.S.
(s/def ::justprint
#(do
; "vec" to realize the LazySeq, which is not realized by join
(print (d/join [ "::justprint ▶ " (vec %) "\n"] ))
true))
(s/def ::mustbe-vec-of-2-int
(s/coll-of integer? :kind vector? :count 2))
Is this correct style and expected to work?
Yes. Specs, keywords that can be resolved to specs, and plain predicate functions can be used interchangeably in many parts of the clojure.spec API. "Pred" in the context of that docstring has a broader meaning than a general Clojure predicate function e.g. nil?.
(require '[clojure.spec.alpha :as s])
(s/conform int? 1) ;; => 1
(s/conform int? false) ;; => :clojure.spec.alpha/invalid
s/def modifies the clojure.spec registry, associating the keyword with the spec value. When you pass ::some-spec-keyword to clojure.spec API it resolves the spec values themselves from the registry. You can also "alias" specs e.g. (s/def ::foo ::bar).
(s/def ::even-int? (s/and int? even?))
(s/conform ::even-int? 2) ;; => 2
(s/conform ::even-int? 3) ;; => :clojure.spec.alpha/invalid
So these are all equivalent:
(s/conform (s/coll-of (s/and int? even?)) [2 4 6 8])
(s/conform (s/coll-of ::even-int?) [2 4 6 8])
(s/def ::coll-of-even-ints? (s/coll-of ::even-int?))
(s/conform ::coll-of-even-ints? [2 4 6 8])

clojure spec for hash-map with interdependent values?

I want to write a clojure spec for a hash-map wherein the value of one of the
keys is constrained to be equal to the sum of the values of two other keys. I
know one way to write a test generator for such a spec by hand:
(ns my-domain)
(require '[clojure.test :refer :all ]
'[clojure.spec.alpha :as s ]
'[clojure.spec.gen.alpha :as gen ]
'[clojure.pprint :refer (pprint) ])
(s/def ::station-id string?)
(s/def ::sim-time (s/double-in :infinite? true, :NaN? false))
(s/def ::reserved-counts (s/and int? #(not (neg? %))))
(s/def ::free-counts (s/and int? #(not (neg? %))))
(def counts-preimage (s/gen (s/keys :req [::station-id
::sim-time
::reserved-counts
::free-counts])))
(pprint (gen/generate
(gen/bind
counts-preimage
#(gen/return
(into % {::total-counts
(+ (::reserved-counts %)
(::free-counts %))})))))
#:my-domain{:station-id "sHN8Ce0tKWSdXmRd4e46fB",
:sim-time -3.4619293212890625,
:reserved-counts 58,
:free-counts 194,
:total-counts 252}
But I haven't figured out how to write a spec for it, let alone a spec that
produces a similar generator. The gist of the problem is that I lack, in the space of specs, a way to
get hold of the "preimage" in the spec, that is, I lack an analogue to bind
from the space of generators. Here is a failed attempt:
(s/def ::counts-partial-hash-map
(s/keys :req [::station-id
::sim-time
::reserved-counts
::free-counts]))
(s/def ::counts-attempted-hash-map
(s/and ::counts-partial-hash-map
#(into % {::total-counts (+ (::reserved-counts %)
(::free-counts %))})))
(pprint (gen/generate (s/gen ::counts-attempted-hash-map)))
#:my-domain{:station-id "ls5qBUoF",
:sim-time ##Inf,
:reserved-counts 56797960,
:free-counts 17}
The generated sample conforms to the spec because #(into % {...}) is truthy,
but the result doesn't contain the new attribute with the key ::total-counts.
I'd be grateful for any guidance.
EDIT: Today I Learned about s/with-gen, which will allow me to attach
my (working) test generator to my "preimage" or "partial" spec. Perhaps
that's the best way forward?
You could use the nat-int? predicate (for which there's a built-in spec, thanks #glts) for the count keys, and add a ::total-counts spec too:
(s/def ::reserved-counts nat-int?)
(s/def ::free-counts nat-int?)
(s/def ::total-counts nat-int?)
(s/def ::counts-partial-hash-map
(s/keys :req [::station-id
::sim-time
::reserved-counts
::free-counts]))
spec for a hash-map wherein the value of one of the keys is constrained to be equal to the sum of the values of two other keys
To add this assertion you can s/and a predicate function with the keys spec (or in this example the merge spec that merges the partial map spec with a ::total-count keys spec):
(s/def ::counts-attempted-hash-map
(s/with-gen
;; keys spec + sum-check predicate
(s/and
(s/merge ::counts-partial-hash-map (s/keys :req [::total-counts]))
#(= (::total-counts %) (+ (::reserved-counts %) (::free-counts %))))
;; custom generator
#(gen/fmap
(fn [m]
(assoc m ::total-counts (+ (::reserved-counts m) (::free-counts m))))
(s/gen ::counts-partial-hash-map))))
This also uses with-gen to associate a custom generator with the spec that sets ::total-count to the sum of the other count keys.
(gen/sample (s/gen ::counts-attempted-hash-map) 1)
=> (#:user{:station-id "", :sim-time 0.5, :reserved-counts 1, :free-counts 1, :total-counts 2})
The generated sample conforms to the spec because #(into % {...}) is truthy, but the result doesn't contain the new attribute with the key ::total-counts.
I'd recommend against using specs to calculate/add ::total-counts to the map. Specs generally shouldn't be used for data transformation.

How to create a spec where all keys are optional but at least one of the specified keys should be present?

How am I supposed to create a spec where all keys are optional but at least one of the specified keys should be present?
(s/def ::my-spec (s/and (help-plz??)(s/keys :opt-un [::a ::b])))
(s/valid? ::my-spec {} => false
(s/valid? ::my-spec {:a 1}) => true
(s/valid? ::my-spec {:b 1}) => true
(s/valid? ::my-spec {:a 1 :b 1}) => true
(s/valid? ::my-spec {:A1 :B 1}) => true
With the current spec alpha, in order to use the same key collection for both the keys spec and the at-least-one-exists check, you'll need to use a macro. (The upcoming spec 2 alpha addresses this by exposing more data-driven APIs for creating specs.)
Here's a quick sketch for your particular example:
(defmacro one-or-more-keys [ks]
(let [keyset (set (map (comp keyword name) ks))]
`(s/and (s/keys :opt-un ~ks)
#(some ~keyset (keys %)))))
(s/def ::my-spec (one-or-more-keys [::foo ::bar]))
(s/conform ::my-spec {:bar nil})
=> {:bar nil}
(s/conform ::my-spec {:baz nil})
=> :clojure.spec.alpha/invalid
Alternatively, you could just define the key collection twice, and use a similar predicate with s/and.
Per the docs for keys:
The :req key vector supports 'and' and 'or' for key groups:
(s/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
Your code should be:
(s/def ::my-spec (s/keys :req-un [(or ::a ::b)]))

How to specify that two keys in a map should have the same value with Clojure.Spec?

Say for a minimal example, I've got a map with the following fields.
{:name
:password
:confirm-password}
and I've written the following specs for this shape.
(s/def ::name string?)
;; password is a string and between 8 - 255 characters
(s/def ::password (s/and string? #(<= 8 (count %) 255))
;; How to write (s/def ::confirm-password)
(s/def ::sign-up-form (s/keys :req-un [::name
::password
::confirm-password])
How would I go about writing a ::confirm-password spec to check whether the two values are equal? i.e. I need access to that other field (password) to get to it.
One thing I tried was to write the spec on the sign-up-form to get access to the keys to make sure they were the same and that kind of works but the problem with that is I lose the path specificity. Basically the spec/problem that get's generated points towards the sign-up form rather than the ::confirm-password which I would like ideally.
You can s/and another predicate with your s/keys spec to check equality between the two keys' values:
(s/def ::sign-up-form
(s/and
(s/keys :req-un [::name
::password
::confirm-password])
#(= (:password %) (:confirm-password %))))
This anonymous function predicate receives the entire conformed map output of the s/keys spec.
(s/explain ::sign-up-form
{:name "Taylor"
:password "weak pass"
:confirm-password "weak pass!"})
;; val: {:name "Taylor", :password "weak pass", :confirm-password "weak pass!"}
;; fails spec: :sandbox.so/sign-up-form predicate:
;; (= (:password %) (:confirm-password %))

clojure spec: map containing either a :with or a :height (XOR)

The following clojure spec ::my permits maps having either the key :width or the key :height, however it does not permit having both of them:
(s/def ::width int?)
(s/def ::height int?)
(defn one-of-both? [a b]
(or (and a (not b))
(and b (not a))))
(s/def ::my (s/and (s/keys :opt-un [::width ::height])
#(one-of-both? (% :width) (% :height))))
Even if it does the job:
(s/valid? ::my {})
false
(s/valid? ::my {:width 5})
true
(s/valid? ::my {:height 2})
true
(s/valid? ::my {:width 5 :height 2})
false
the code does not appear that concise to me. First the keys are defined as optional and then as required. Does anyone have a more readable solution to this?
clojure.spec is designed to encourage specs capable of growth. Thus its s/keys does not support forbidding keys. It even matches maps having keys that are neither in :req or opt.
There is however a way to say the map must at least have :width or :height, i.e. not XOR, just OR.
(s/def ::my (s/keys :req-un [(or ::width ::height)]))
This feature is built into spec - you can specify and/or patterns in req-un:
(s/def ::my (s/keys :req-un [(or ::width ::height)]))
:user/my
user=> (s/valid? ::my {})
false
user=> (s/valid? ::my {:width 5})
true
user=> (s/valid? ::my {:height 2})
true
user=> (s/valid? ::my {:width 5 :height 2})
true
Just wanted to pitch in with a small modification to the spec in the original question which logic will fail if the value held by any key is falsey, i.e. false or nil.
(spec/valid? ::my {:width nil})
=> false
Of course this won't occur with the given constraints of int? put on the values in the question. But perhaps someone in posterity allows their values to be nilable or boolean in which case this answer becomes handy.
If we instead define the spec as:
(defn xor? [coll a-key b-key]
(let [a (contains? coll a-key)
b (contains? coll b-key)]
(or (and a (not b))
(and b (not a)))))
(spec/def ::my (spec/and (spec/keys :opt-un [::width ::height])
#(xor? % :width :height)))
we get the result that
(spec/valid? ::my {:width nil})
=> true
(spec/valid? ::my {:width nil :height 5})
=> false