1
votes

I have a spec definition that validates contents of incoming data. Since the data is a map of fields, I use spec/keys for validating it. For example:

(def person-data {:name "Jon Doe", :age 30})
(s/def ::name string?)
(s/def ::age pos-int?)
(s/def ::person-info (s/keys :req-un [::name ::age])
...
;validate data via spec and make sure no additional keys are included
(s/valid? ::person-spec some-input)

But an additional need I have is to make sure incoming data only contains the keys I want. (in this case :name and :age keys only. For that, I do something like:

(def permitted-keys [:age :name])
(select-keys some-input permitted-keys)

, ensuring only those keys get filtered in.

Is there a way I can reuse some code between my spec definition for the map structure (s/keys) and this additional step I take for filtering the allowed keys (permitted-keys)?

Perhaps by either extracting the list of keys from the s/keys definition, or by passing an existing vector of keys to s/keys?

1
Check out this macro. - Taylor Wood

1 Answers

1
votes

I'd recommend looking at this macro as it covers all the bases and provides a generator, but here's another take that assumes your maps only used unnamespaced keys:

(defmacro keys-strict
  [& args]
  (let [{:keys [req opt req-un opt-un]} args
        ks (into #{} (->> (concat req opt req-un opt-un)
                          (map #(keyword (name %)))))] ;; strip namespaces from keywords
    `(s/and (s/keys ~@args) (s/map-of ~ks any?))))

The only trick here for reusing the same source of truth for the keys is that your key specs will be namespaced but your map keys will not. You could do the same without a macro, you just s/and your s/keys spec with a s/map-of spec or some other spec that restricts the keys allowed.

Is there a way I can reuse some code between my spec definition for the map structure (s/keys) and this additional step I take for filtering the allowed keys (permitted-keys)?

Yes, this is handled in the example above by splicing args onto the s/keys call, and done similarly in this more complete macro here.

Note: There may be situations where you really need to restrict what keys you'll accept, but I think it's generally advised to define map specs that are open for later extension.