validationdictionaryclojurespecificationsclojure.spec

Validate map of known and unknown key-values with Spec


I want to create a clojure spec to validate :multipart parameter of an http request, created by reitit.ring.middleware.multipart middleware.

The multipart form data must contain particular parameters, which could be validated with s/keys, and any number of files with arbitrary parameter name.

The map to validate would look like this:

           {:visualisation "vis"
            :file-xy       {:filename     "foo.png",
                            :content-type "image/png",
                            :tempfile     "C:\\Temp\\ring-multipart-123.tmp",
                            :size         295281}
            :file-abc      {:filename     "bar.png",
                            :content-type "image/png",
                            :tempfile     "C:\\Temp\\ring-multipart-456.tmp",
                            :size         42}}

I can validate the files with reitit.ring.middleware.multipart/temp-file-part spec like this:

    (s/def :multipart/files (s/map-of :multipart/param multipart/temp-file-part))

Putting it together, I came up with a spec which passes, but it allows all unknown parameters to be either file or string:

(s/def :multipart/param keyword?)
(s/def :multipart/visualisation string?)
(s/def :multipart/items (s/map-of :multipart/param (s/or :file multipart/temp-file-part :visualisation string?)))
(s/def :visualisation/files (s/and (s/keys :req-un [:multipart/visualisation])
                                   :multipart/items))

How can I define a spec for a map with specific keys and a value validator for other keys?


Solution

  • You could add a predicate function to ensure only one of the map's values is a string:

    (s/def :visualisation/files
      (s/and (s/keys :req-un [:multipart/visualisation])
             #(= 1 (count (filter string? (vals %))))
             :multipart/items))
    
    (s/conform :visualisation/files
      {:visualisation "vis"
       :file-xy       {:filename "" :content-type "" :tempfile "" :size 0}})
    
    (s/conform :visualisation/files
      {:visualisation "vis"
       :file-abc      "not valid"
       :file-xy       {:filename "" :content-type "" :tempfile "" :size 0}})
    

    But the catch-all s/or spec will produce redundant tags if you're using the conform'd output.

    Another option could be to use two spec checks:

    (def data
      {:visualisation "vis"
       :file-xy       {:filename "" :content-type "" :tempfile "" :size 0}})
    
    (s/conform (s/keys :req-un [:multipart/visualisation])
               data)
    
    (s/conform (s/map-of :multipart/param :multipart/temp-file-part)
               (dissoc data :visualisation))
    

    If you control the shape of the data or can rearrange it, I'd consider something like this which would be easier to spec:

    {:visualisation "foo"
     :files {:file-1 {,,,}
             :file-2 {,,,}}}