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?
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 {,,,}}}