Is there a way to simply specify that a collection of routes share a response spec, such as a 401? It seems like the :parameters key accrues like you would expect, but the :responses key cannot because it is part of the data of a particular request method.
Here is a rough sketch of what I am talking about:
["/pets" {}
["/dog" {:get {:handler (constantly {:status 200 :body "spot"})
:responses {200 {:body #{"spot"}}
401 {:body #{"Unauthorized"}}}}}]
["/cat" {:get {:handler (constantly {:status 200 :body "fluffy"})
:responses {200 {:body #{"fluffy"}}
401 {:body #{"Unauthorized"}}}}}]]
I want to configure this route tree so that all child methods of /pets/* may have a 401 responseGood question! I don't see any reason why the responses map couldn't get merged. I'll dig into the code.
Meanwhile, since the route tree is data, you could write a function that walks it and adds the necessary 401s ... if you feel that the added complexity is worth the guaranteed response schemas 🙂
This seems to work:
user> (def router (r/router ["/api" {:get {:responses {401 {:description "A default 401 response"}}}}
["/foo" {:get {:handler identity
:responses {200 {:description "A specific 200 response"}}}}]
["/bar" {:get {:handler identity
:responses {200 {:description "Another 200 response"}}}}]]))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
{:get
{:responses
{401 {:description "A default 401 response"},
200 {:description "A specific 200 response"}},
:handler
#object[clojure.core$identity 0x421c9c86 "clojure.core$identity@421c9c86"]}}]
["/api/bar"
{:get
{:responses
{401 {:description "A default 401 response"},
200 {:description "Another 200 response"}},
:handler
#object[clojure.core$identity 0x421c9c86 "clojure.core$identity@421c9c86"]}}]]unfortunately you'll have to handle every method separately, I think
The above caused me to re-investigate this. The accumulation of responses actually seems to work, but if you use the default route data specs, then route data validation fails. So, if you add a :responses key to the spec in your router options, everything seems to work. I am not sure if the ::reitit.ring.spec/data spec should be expanded to include an optional :responses key.
I'm not really following you. This seems to work:
user> (def router (r/router ["/api"
["/foo" {:get {:handler identity
:responses {200 {:description "A specific 200 response"}}}}]
["/bar" {:post {:handler identity
:responses {200 {:description "Another 200 response"}}}}]]
{:data {:get {:responses {401 {:description "A default 401 response"}}}
:post {:responses {401 {:description "A default 401 response"}}}}
:validate reitit.spec/validate}))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
{:get
{:responses
{401 {:description "A default 401 response"},
200 {:description "A specific 200 response"}},
:handler
#object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"]},
:post {:responses {401 {:description "A default 401 response"}}}}]
["/api/bar"
{:get {:responses {401 {:description "A default 401 response"}}},
:post
{:responses
{401 {:description "A default 401 response"},
200 {:description "Another 200 response"}},
:handler
#object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"]}}]]
nilNo wait, I screwed up the validation...
No, it still passes even with reitit.ring.spec/validate, since the spec for :get (etc) is just map?
I never use validation myself so I might just be lost in the API here 😅
Well here we go:
user> (def router (reitit.ring/router ["/api"
["/foo" {:get {:handler identity
:responses {200 {:description "A specific 200 response"}}}}]
["/bar" {:post {:handler identity
:responses {200 {:description "Another 200 response"}}}}]]
{:data {:get {:responses {401 {:description "A default 401 response"}}}
:post {:responses {401 {:description "A default 401 response"}}}}
:validate reitit.ring.spec/validate}))
Execution error (ExceptionInfo) at reitit.exception/exception (exception.cljc:19).
path "/api/foo" doesn't have a :handler defined for :post
{:path "/api/foo", :data {:responses {401 {:description "A default 401 response"}}}, :scope :post}so using default route data doesn't work unless you have the same methods defined for every endpoint
Thanks for the help. That does seem to fail if one of the child routes does not have a :get method, for example:
(ring/router ["/api" {:get {:responses {401 {:description "A default 401 response"}}}}
["/foo" {:put {:handler identity
:responses {200 {:description "A specific 200 response"}}}}]
["/bar" {:get {:handler identity
:responses {200 {:description "Another 200 response"}}}}]])
;; => Execution error (ExceptionInfo) at reitit.exception/exception (exception.cljc:19).
;; path "/api/foo" doesn't have a :handler defined for :get
;; {:path "/api/foo", :data {:responses {401 {:description "A default 401 response"}}}, :scope :get}
I think walking the tree may be the most elegant solution, but I think I will just make a helper function that takes method data and adds these common responses for our API.
Thanks again!Oh, right, good point about the missing methods. Maybe if reitit-ring was designed a bit differently you could have shared :responses next to :get instead of inside it...
yeah I think that would be the best way to add support to this: allow a :responses field in the "route-data" that gets merged with the method data. I can make an issue to at least track this idea if you also agree.
Are you sure it can't? I'd swear I have some "global" 401 definitions like that
nope:
user> (def router (r/router ["/api" {:responses {401 {:description "A default 401 response"}}}
["/foo" {:get {:handler identity
:responses {200 {:description "A specific 200 response"}}}}]
["/bar" {:post {:handler identity
:responses {200 {:description "Another 200 response"}}}}]]))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
{:responses {401 {:description "A default 401 response"}},
:get
{:handler
#object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"],
:responses {200 {:description "A specific 200 response"}}}}]
["/api/bar"
{:responses {401 {:description "A default 401 response"}},
:post
{:handler
#object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"],
:responses {200 {:description "Another 200 response"}}}}]]though maybe you have a trick that works?
Pushed out a new reitit release using an automated release workflow: 0.9.2-rc1. Testing welcome! If it seems fine, I'll make a proper release on monday.