Search code examples
clojureclojure.spec

Clojure spec for a single key in a map


I'm speccing http responses from Google Calendar API and I want different specs for each response type.

I've defined a spec for HTTP response as

(s/def ::http-resp
  (s/keys :req-un [:status] :opt-un [:body]))

But how do I define a spec for each HTTP status? I know I can do :s.http-error-401/status, but I would prefer something like

(s/and ::http-response 
       (key-in-a-map :status :s.http-statuses.error/gone))

Maybe there's an example of good specs for HTTP responses? I've found only ring-spec so far.


Solution

  • IMO ring-spec already serve your purpose. You can find the spec for HTTP status there:

    (s/def :ring.response/status (s/int-in 100 600))
    

    Note that HTTP status code is a number. If you need to fine tune the spec, you can always do something like this:

    (def ^:const OK 200)
    (def ^:const UNAUTHORIZED 401)
    
    (s/def ::status #{OK UNAUTHORIZED})
    
    (s/def ::body string?)
    
    (s/def ::response (s/keys :req-un [::status] :opt [::body]))
    
    (s/valid? ::response {:status 200
                          :body   "Hello"})
    ;; => true
    
    
    (s/valid? ::response {:status 1000
                          :body   "Hello"})
    ;; => false
    
    

    Updated based on comment from 2021.09.26

    You can also define each return code as individual spec and combine them with or. Then you can use conform to get the code:

    
    
    (s/def ::status (status-spec [200 OK
                                  400 BAD_REUEST
                                  404 NOT_FOUND]))
    
    (s/def ::response (s/keys :req-un [::status] :opt [::body]))
    
    (s/conform ::response {:status 200
                           :body "Hello"})
    ;; => {:status [:user/OK 200], :body "Hello"}
    
    (s/conform ::status 200)
    ;; => [:user/OK 200]
    
    

    Macro to make defining individual return code spec easier:

    (defn- destructure-kv [kvs]
      (let [xs (partition 2 kvs)]
        (interleave (map (comp (partial keyword (str *ns*))
                               str
                               last)
                         xs)
                    (map (comp set vector first) xs))))
    
    (defmacro status-spec [kvs]
      `(s/or ~@(destructure-kv kvs)))
    

    which basically generate an or:

    (status-spec [200 OK
                  400 BAD_REUEST
                  404 NOT_FOUND])
    
    =>
    
    (s/or :user/OK #{200} 
          :user/BAD_REUEST #{400} 
          :user/NOT_FOUND #{404})