Fork me on GitHub
#clojure-spec
<
2023-05-10
>
Henrique Prange15:05:35

I have created specifications for a file entity, with separate definitions for documents and images. To differentiate between the two, I used s/or to define the :file/content-type according to the media type. However, I’d like to ensure that the :file/content-type matches the media type of the file being checked, depending on whether it is an image or a document. Specifically, I would like to ensure that image files are of content-type "image/jpeg" or "image/png" only, and that document files are of content-type "application/pdf" or "image/jpeg" only. Below is the specification I have created:

(s/def :file/content-type (s/or :doc #{"application/pdf" "image/jpeg"}
                                :img #{"image/jpeg" "image/png"}))
(s/def :file/height int?
(s/def :file/width int?
  
(s/def :file/doc (s/keys :req [:file/content-type]))
(s/def :file/img (s/keys :req [:file/content-type :file/height :file/width]))
Can someone please advise on how I can ensure that image files and document files have the appropriate content type?

Mark Wardle18:05:44

Hi Henrique... (Are you using Clojure with WO now?) You can use s/and for this kind of stuff with any predicate(s) you want. If you want, instead of using two different specs, you can combine them into a single spec and then have a multi-spec on the type: (s/def :file/content-type #{"application/pdf" "image/jpeg" "image/png"}) (s/def :file/height int?) (s/def :file/width int?) (s/def :file/type #{:img :doc}) (defmulti file-spec :file/type) (defmethod file-spec :img [_] (s/and (s/keys :req [:file/type :file/content-type :file/height :file/width]) #(#{"image/jpeg" "image/png"} (:file/content-type %)))) (defmethod file-spec :doc [_] (s/and (s/keys :req [:file/type :file/content-type]) #(#{"application/pdf" "image/jpeg"} (:file/content-type %)))) (s/def ::file (s/multi-spec file-spec :file/type)) Here I'm assuming the file data comes with :file/type and file:content-type. Alternatively, you can define two specs as you did and use whichever one you want. When I started, I thought I'd specify everything with clojure spec, but there is a long tail with diminishing returns, so I'll often not specify some things, knowing I can supplement with runtime checks and use spec a la carte.

Mark Wardle18:05:41

This is what it would look like with two separate specs meaning that your code has to make the choice as to which type to check against: (s/def :file/content-type #{"application/pdf" "image/jpeg" "image/png"}) (s/def :file/height int?) (s/def :file/width int?) (s/def :file/doc (s/and (s/keys :req [:file/content-type]) #(#{"application/pdf" "image/jpeg"} (:file/content-type %)))) (s/def :file/img (s/and (s/keys :req [:file/content-type :file/height :file/width]) #(#{"image/jpeg" "image/png"} (:file/content-type %)))) (s/conform :file/doc {:file/content-type "application/pdf"}) (s/conform :file/img {:file/content-type "application/pdf"}) (s/conform :file/doc {:file/content-type "image/jpeg"}) (s/conform :file/img {:file/content-type "image/jpeg" :file/height 100 :file/width 100})

Henrique Prange13:05:25

Hey Mark! Great to hear from you! I’m no longer working with WebObjects, and I’m now using Clojure and Datomic on my current project. Thanks for your help with the spec question. I’m using spec mostly to validate that my Datomic entities are complete and consistent, and it’s been working really well so far. I didn’t know that I could use defmulti with spec, which is really helpful! Ultimately, I ended up following your second suggestion and making separate specifications for images and documents using s/and. Thanks again for your advice!

👍 2
Mark Wardle14:05:39

No problem at all. I still have a legacy WO application in production that I'm replacing bit by bit over time with new Clojure based components. If I squint, I see the older dynamism of the objective C WO and the key value coding / key paths form WO in Clojure and its general approach, although I sometimes miss the batteries-included approach on occasions, I definitely feel I'm not fighting the framework as much here.