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?

+3


source to share


3 answers


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 used map-of

    above and keys

    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!

+4


source


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.

+4


source


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

.

+2


source







All Articles