Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I spec a hybrid map?

After writing this answer, I was inspired to try to specify Clojure's destructuring language using spec:

(require '[clojure.spec :as s])

(s/def ::binding (s/or :sym ::sym :assoc ::assoc :seq ::seq))

(s/def ::sym (s/and simple-symbol? (complement #{'&})))

The sequential destructuring part is easy to spec with a regex (so I'm ignoring it here), but I got stuck at associative destructuring. The most basic case is a map from binding forms to key expressions:

(s/def ::mappings (s/map-of ::binding ::s/any :conform-keys true))

But Clojure provides several special keys as well:

(s/def ::as ::sym)
(s/def ::or ::mappings)

(s/def ::ident-vec (s/coll-of ident? :kind vector?))
(s/def ::keys ::ident-vec)
(s/def ::strs ::ident-vec)
(s/def ::syms ::ident-vec)

(s/def ::opts (s/keys :opt-un [::as ::or ::keys ::strs ::syms]))

How can I create an ::assoc spec for maps that could be created by merging together a map that conforms to ::mappings and a map that conforms to ::opts? I know that there's merge:

(s/def ::assoc (s/merge ::opts ::mappings))

But this doesn't work, because merge is basically an analogue of and. I'm looking for something that's analogous to or, but for maps.

like image 409
Sam Estep Avatar asked Jul 01 '16 18:07

Sam Estep


2 Answers

You can spec hybrid maps using an s/merge of s/keys and s/every of the map as tuples. Here's a simpler example:

(s/def ::a keyword?)
(s/def ::b string?)
(s/def ::m
  (s/merge (s/keys :opt-un [::a ::b])
           (s/every (s/or :int (s/tuple int? int?)
                          :option (s/tuple keyword? any?))
                    :into {})))

(s/valid? ::m {1 2, 3 4, :a :foo, :b "abc"}) ;; true

This simpler formulation has several benefits over a conformer approach. Most importantly, it states the truth. Additionally, it should generate, conform, and unform without further effort.

like image 89
Alex Miller Avatar answered Nov 01 '22 13:11

Alex Miller


You can use s/conformer as an intermediate step in s/and to transform your map to the form that’s easy to validate:

(s/def ::assoc
  (s/and
    map?
    (s/conformer #(array-map
                    ::mappings (dissoc % :as :or :keys :strs :syms)
                    ::opts     (select-keys % [:as :or :keys :strs :syms])))
    (s/keys :opt [::mappings ::opts])))

That will get you from e.g.

{ key :key
  :as name }

to

{ ::mappings { key :key }
  ::opts     { :as name } }
like image 28
Niki Tonsky Avatar answered Nov 01 '22 12:11

Niki Tonsky