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

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

Related

Test for a Valid Instance of java.time.LocalDate using Clojure Spec

I am trying to use Clojure Spec to define a data structure containing a java.time.LocalDate element:
(s/def :ex/first-name string?)
(s/def :ex/last-name string?)
(s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))
(s/def :ex/person
(s/keys :req [:ex/first-name
:ex/last-name
:ex/birth-date]))
(def p1 #:ex{:first-name "Jenny"
:last-name "Barnes"
:birth-date (java.time.LocalDate/parse "1910-03-15")})
(println p1)
produces the following output
#:ex{:first-name Jenny, :last-name Barnes, :birth-date #object[java.time.LocalDate 0x4ed4f9db 1910-03-15]}
However, when I test to see if p1 conforms to the :ex/person spec, it fails:
(s/valid? :ex/person p1)
ClassCastException java.lang.Boolean cannot be cast to clojure.lang.IFn clojure.spec.alpha/spec-impl/reify--1987 (alpha.clj:875)
Looking closer at the Clojure examples for inst?, I see:
(inst? (java.time.Instant/now))
;;=> true
(inst? (java.time.LocalDateTime/now))
;;=> false
However, I don't see an obvious reason as to why that returns false. This seems to be the root of my issue, but I have not found a solution and would like some help.
You're probably looking for instance?- and your example fails, because in:
(s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))
this part (s/valid? inst? (java.time.LocalDate/now)) should be a function (predicate), not boolean. The full code:
(s/def :ex/first-name string?)
(s/def :ex/last-name string?)
(s/def :ex/birth-date #(instance? java.time.LocalDate %))
(s/def :ex/person
(s/keys :req [:ex/first-name
:ex/last-name
:ex/birth-date]))
(def p1 #:ex{:first-name "Jenny"
:last-name "Barnes"
:birth-date (java.time.LocalDate/parse "1910-03-15")})
(s/valid? :ex/person p1)
=> true
inst? won't work here, because Inst is a protocol, used to extend java.util.Date and java.time.Instant:
(defprotocol Inst
(inst-ms* [inst]))
(extend-protocol Inst
java.util.Date
(inst-ms* [inst] (.getTime ^java.util.Date inst)))
(defn inst?
"Return true if x satisfies Inst"
{:added "1.9"}
[x]
(satisfies? Inst x))
(extend-protocol clojure.core/Inst
java.time.Instant
(inst-ms* [inst] (.toEpochMilli ^java.time.Instant inst)))
And you can use satisfies? to check whether some object satisfies given protocol:
(satisfies? Inst (java.time.LocalDate/parse "1910-03-15"))
=> false

Can I merge two keys specs requiring the same unqualified key?

In the same namespace I define the following specs:
(s/def :foo/x string?)
(s/def :foo/o (s/keys :req-un [:foo/x]))
(s/valid? :foo/o {:x "true"})
;=> true
(s/def :bar/x boolean?)
(s/def :bar/o (s/keys :req-un [:bar/x]))
(s/valid? :bar/o {:x true})
;=> true
Both :foo/o and :bar/o require different unqualified keys. (I just didn't include them to keep things simple.)
Now I want to create :baz/o from :foo/o and :bar/o and went for the following spec: (still in the same namespace)
(s/def :baz/o (s/merge :foo/o :bar/o))
Assuming that in case of key collisions :bar/o would take precedence (which is what I want). However the resulting spec rejects both strings and booleans:
(s/valid? :baz/o {:x true})
;=> false
(s/valid? :baz/o {:x "true"})
;=> false
Here are the results from s/explain:
(s/explain :baz/o {:x "true"})
; "true" - failed: boolean? in: [:x] at: [:x] spec: :bar/x
(s/explain :baz/o {:x true})
; true - failed: string? in: [:x] at: [:x] spec: :foo/x
It does seem like s/merge has created a predicate for :x that I can't satisfy.
In case of "collision" :bar/o should take precedence. How can I achieve that? (I'm on Clojure 1.10)

Combining s/and with s/or in Clojure spec

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

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)]))

clojure.spec coll-of alternative types

I'm using clojure.spec to validate a vector of map-entries. The vector looks like:
[{:point {:x 30 :y 30}}
{:point {:x 34 :y 33}}
{:user "joe"}]
I'd like to structure the spec to require 1..N ::point entries and only a single ::user entry.
Here is my (unsuccessful) attempt at structuring this spec:
(s/def ::coord (s/and number? #(>= % 0)))
(s/def ::x ::coord)
(s/def ::y ::coord)
(s/def ::point (s/keys :req-un [::x ::y]))
(s/def ::user (s/and string? seq))
(s/def ::vector-entry (s/or ::pt ::user))
(s/def ::my-vector (s/coll-of ::vector-entry :kind vector))
When I run just the validation of one ::point entry, it works:
spec> (s/valid? ::point {:point {:x 0 :y 0}})
true
spec> (s/valid? ::my-vector [{:point {:x 0 :y 0}}])
false
Any ideas on how to structure the s/or part so the vector entries can be of either ::user or ::point types?
Also, any ideas on how to require one and only one ::user entry and 1..N ::point entries in the vector?
Here is a possible spec for the data in your question:
(require '[clojure.spec.alpha :as s])
(s/def ::coord nat-int?)
(s/def ::x ::coord)
(s/def ::y ::coord)
(s/def ::xy (s/keys :req-un [::x ::y]))
(s/def ::point (s/map-of #{:point} ::xy))
(s/def ::username (s/and string? seq))
(s/def ::user (s/map-of #{:user} ::username))
(s/def ::vector-entry (s/or :point ::point :user ::user))
(s/def ::my-vector (s/coll-of ::vector-entry :kind vector))
(s/valid? ::point {:point {:x 0 :y 0}})
(s/valid? ::my-vector [{:point {:x 0 :y 0}}])
(s/valid? ::my-vector [{:point {:x 0 :y 0}} {:user "joe"}])
A few observations:
An or spec requires that specs be given names.
The labelling of the different items by type :point or :user necessitates a level of indirection, I used map-of on the top and keys for the nested level but there are many choices
The small errors in your specs could be caught early by trying each subform at the REPL.
In this case the relative difficulty of specing the data is a hint that this data shape will be inconvenient for programs, too. Why force a program to do an O(N) search when you know :user is required?
Hope this helps!
While Stuart's answer is very instructive and solves many of your problems, I don't think it covers your criteria of ensuring "one and only one ::user entry."
Riffing off his answer:
(s/def ::coord nat-int?)
(s/def ::x ::coord)
(s/def ::y ::coord)
(s/def ::xy (s/keys :req-un [::x ::y]))
(s/def ::point (s/map-of #{:point} ::xy))
(s/def ::username (s/and string? seq))
(s/def ::user (s/map-of #{:user} ::username))
(s/def ::vector-entry (s/or :point ::point
:user ::user))
(s/def ::my-vector (s/and (s/coll-of ::vector-entry
:kind vector)
(fn [entries]
(= 1
(count (filter (comp #{:user}
key)
entries))))))
(s/valid? ::point {:point {:x 0 :y 0}})
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}])
;; => false
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:user "joe"}])
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:point {:x 1 :y 1}}
{:user "joe"}])
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:user "joe"}
{:user "frank"}])
;; => false
The important addition is in the spec for ::my-vector. Note that the conformed output of s/or is a map entry, and that is what is passed to the new custom predicate.
I should note that, while this works, it adds yet another linear scan to your validation. Unfortunately, I don't know if spec provides a good way to do it in a single pass.
The answers by Tim and Stuart solved the issue and were very informative. I would like to point out that there is also a feature of Clojure spec which can be used to specify structure for the vector of points and a user.
Namely, spec allows to use regular expressions to specify sequences. For more information see the Spec guide.
Below is a solution using specs for sequences. This builds on the previous solutions.
(require '[clojure.spec.alpha :as s])
(s/def ::coord nat-int?)
(s/def ::x ::coord)
(s/def ::y ::coord)
(s/def ::xy (s/keys :req-un [::x ::y]))
(s/def ::point (s/map-of #{:point} ::xy))
(s/def ::username (s/and string? seq))
(s/def ::user (s/map-of #{:user} ::username))
(s/def ::my-vector (s/cat :points-before (s/* ::point)
:user ::user
:points-after (s/* ::point)))
(s/valid? ::point {:point {:x 0 :y 0}})
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}])
;; => false
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:user "joe"}])
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:point {:x 1 :y 1}}
{:user "joe"}])
;; => true
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:user "joe"}
{:user "frank"}])
;; => false
(s/valid? ::my-vector [{:point {:x 0 :y 0}}
{:user "joe"}
{:point {:x 1 :y 1}}])
;; => true
This can be easily adapted if the ::user entry is required at the end.