Fork me on GitHub
#clojure-spec
<
2024-04-05
>
jjttjj14:04:06

I'm trying to spec this situation, where I have maps with a :type keyword that correspond to an actual "spec", and a :request which is that value that should be "spec'ed" by it. I'm thinking at this point that it involves a combination of multi-spec and conform/unform/conformer but I'm not quite figuring out how to put it all together. Any tips?

(def req1
    {:type    :x
     :request {:a 1}})

  (def req2
    {:type    :y
     :request {:b 1}})

  (s/def ::a int?)
  (s/def ::b int?)


  (s/def ::conformed-request
    (s/conformer
      (fn [{:keys [type request] :as m}]
        [type request])
      (fn [[type request]]
        {:type type :request request})))

  (->> {:type    :x
        :request {:a 1}}
       (s/conform ::conformed-request)
       #_(s/unform ::conformed-request))

  (defmulti request-spec :type)

  (defmethod request-spec :x [_]
    ;; not correct, this is the spec for :request in a map with :type :x
    ;; I could first conform to [spec-key value] and then validate that. Is there a way to do that without predicates?
    #_(s/keys :req-un [::a]))

Alex Miller (Clojure team)16:04:30

does not seem to use s/multi-spec ?

jjttjj16:04:47

But in that case and most the examples I've found, the multimethod returns some spec for the overall thing that is being dispatched on. In my case the data is nested, so I have maps with :type and :request and :type is a spec-key and :request is the thing to be spec'ed according to that key So I could dispatch on :type in the multimethod but then i'd need to say in the defmethod impl to return a spec that says "in this context, the :request key should be spec'd according to the dispatch value. But I don't think that's possible so don't think multi-spec is the answer here, or I'm looking at it wrong. I could preprocess this data to turn it into {spec-key request-data} and then call s/valid? on it, but I'm wondering if there's a better way.

(defmulti request-spec :type)

(defmethod request-spec :x [_]
  ;; this is the spec for the value at the :request key when type is :x
  #_(s/keys :req-un [::a])

  ;; what actually goes here?
  )

(s/def ::request-data
  (s/and (s/keys :req-un [::request ::type])
         (s/multi-spec request-spec :type)))

Alex Miller (Clojure team)17:04:52

I think the basic mismatch is that :request means more than one thing here. generally spec expects a key to mean on thing. however, I think you could handle this by: • creating a ::x-req-spec for :request when spec is x, and an overall message spec ::x-msg for the map using it • creating a ::y-req-spec for :request when spec is y, and an overall message spec ::y-msg for the map using it • have each defmethod return the latter

jjttjj17:04:12

Ah that makes sense, thanks! And just to make sure, conformers definitely aren't the right path to go down here right? Was starting to mess around with this, but not really getting anywhere

(s/def ::conformed-request
  (s/conformer
    (fn [{:keys [type request] :as m}]
      (if-not (and type request)
        ::s/invalid
        (assoc m
          type request)))))

(s/def ::x (s/keys :req-un [::a]))
(s/def ::y (s/keys :req-un [::b]))

(s/def :outer/request
  (s/and
    (s/keys :req-un [:inner/request ::type])
    ::conformed-request
    ;;was hoping this would "recheck" the conformed thing but doesn't seem to work that way
    (s/keys)))

Alex Miller (Clojure team)17:04:56

conformers are almost never the right path to go down :)

jjttjj17:04:37

I shall conform to that mindset then. Thanks again for the advice