Clojure.spec coll - alternative types
I am using clojure.spec to check a vector of cartographic records. The vector looks like this:
[{:point {:x 30 :y 30}}
{:point {:x 34 :y 33}}
{:user "joe"}]
I would like to structure the specification to require 1..N entry ::point
and only one entry ::user
.
Here's my (unsuccessful) attempt at structuring this specification:
(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 only run validation of one entry ::point
, 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 part s/or
so that vector elements can be ::user
either ::point
types or types?
Also, any ideas on how to require one and only one record ::user
and 1..N ::point
records in a vector?
source to share
Here is a possible specification 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:
- The specification
or
requires specifications to be specified by name. - Labeling different elements by type
:point
or:user
requires a level of indirection, I usedmap-of
above andkeys
for the nested level, but there are many options - Small errors in your specs can be discovered early on by trying each subform in the REPL .
- In this case, the relative complexity of data presentation is a hint that this form of data will be inconvenient for programs. Why would you want a program to do O (N) lookups when you know what
:user
is required?
Hope this helps!
source to share
While Stewart's answer is very instructive and solves many of your problems, I don't think it covers your criteria for providing "one and only one entry ::user
."
By submitting your 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
An important addition is the specification for ::my-vector
. Note that the consistent output s/or
is a map entry and that is what is passed to the new custom predicate.
I should point out that while this works, it adds another line scan to your check. Unfortunately I don't know if the spec gives a good way to do this in one pass.
source to share
Tim and Stewart's answers solved the problem and were very informative. I would like to point out that there is also a Clojure spec function that can be used to specify a structure for a vector of points and a user.
Specifically, spec allows regular expressions to be used to specify sequences. For more information, see the Specification .
Below is a solution using sequencing specs. This builds on previous decisions.
(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 a recording is required at the end ::user
.
source to share